Phaser 3의 모듈러 게임 월드 (Tilemaps # 2) - Dynamic Platformer

이것은 Phaser 3 게임 엔진에서 타일 맵을 사용하여 모듈러 월드를 만드는 것에 관한 일련의 블로그 게시물입니다. 아직 읽지 않았다면 정적 타일 맵을 사용하여 포켓몬 스타일의 게임 세계를 만드는 첫 번째 포스트를 확인하십시오. 이 글에서는 역동적 인 타일 맵에 대해 알아보고 장애물을 해결할 수있는 플랫폼을 그릴 수있는 퍼즐 -y 플랫폼을 만듭니다.


다음 글은 절차 적 던전 세계를 만드는 방법을 다루고 있으며, 이후에는 Matter.js를 통합하여 벽을 뛰어 넘는 플랫폼 작성자를 만드는 방법에 대해 다룹니다.

우리가 들어가기 전에이 저장소와 함께가는 모든 소스 코드와 자산을이 저장소에서 찾을 수 있습니다. 이 자습서에서는 02/26/19 현재 Phaser (v3.16.2) 및 Tiled (v1.2.2)의 최신 버전을 사용합니다. Phaser와 Tiled의 이전 버전의 일부 페어링은 제대로 작동하지 않으므로이 두 버전을 사용하는 것이 좋습니다.


대상 독자

이 게시물은 JavaScript (클래스, 화살표 함수 및 모듈), Phaser 및 Tiled 맵 편집기에 대한 경험이있는 사용자에게 가장 적합합니다. 그렇지 않다면 시리즈 초반부에 시작하거나 Google, Phaser 안내서 및 Phaser 예제 및 설명서를 계속 읽고 보관할 수 있습니다.

좋아, 들어 가자!


Tilemap API

플랫폼 작성기를 작성하기 전에 타일 맵 API를 눈으로 보게됩니다. 지난 번에 다음과 같은 비트에 대해 이야기했습니다.
- Tilemap
- Tileset
- StaticTilemapLayer

이 글에서는 두 가지 새로운 API를 소개합니다.
- DynamicTilemapLayer
- Tile

Tilemap은 표시 객체가 아닙니다. 이 맵에는 맵에 대한 데이터가 있으며 실제로 Tile 객체를 렌더링하는 표시 객체 인 하나 이상의 레이어를 포함 할 수 있습니다. StaticTilemapLayer & DynamicTilemapLayer의 두 가지 맛이 있습니다. StaticTilemapLayer는 빠르지 만 해당 레이어의 타일을 수정할 수 없으며 뒤집기 또는 색조와 같은 타일 별 효과를 렌더링 할 수 없습니다. DynamicTilemapLayer는 개별 타일 조작의 유연성과 성능면에서 어느 정도 속도가 비슷합니다.

정적 및 동적 레이어는 동일한 API의 대부분을 공유합니다. 둘 다 타일이 있는지 여부를 확인하는 메소드를 가지고 있습니다 (예 : hasTileAt). 둘 다지도의 타일 (getTileAt, findTile, forEachTile 등)에 액세스하는 메소드를 가지고 있습니다. 동적 레이어에는 레이어 내에서 타일을 추가, 제거, 임의 화하는 등의 추가 메소드 세트가 있습니다 (예 : putTileAt, removeTileAt, randomize 등).


Painting Tiles

첫 번째 예제에서는 Tiled로 만든 레벨을로드 한 다음 타일을 즉시 칠하고 지 웁니다. 여기서 정적 레이어를 사용할 수 없으므로 동적 레이어를 사용해야합니다.


정적 레이어와 같은 방식으로 동적 레이어를 설정합니다 (map.createStynamicLayer 대신 map.createDynamicLayer 사용).
let groundLayer;
function preload() {
this.load.image("tiles", "../assets/tilesets/0x72-industrial-tileset-32px-extruded.png");
this.load.tilemapTiledJSON("map", "../assets/tilemaps/platformer.json");
}
function create() {
const map = this.make.tilemap({ key: "map" });
const tiles = map.addTilesetImage("0x72-industrial-tileset-32px-extruded", "tiles");
map.createDynamicLayer("Background", tiles);
groundLayer = map.createDynamicLayer("Ground", tiles);
map.createDynamicLayer("Foreground", tiles);
}

동적 레이어가로드되면 DynamicTilemapLayer API를 사용하여 타일을 조작 할 수 있습니다.
// Put tile index 1 at tile grid location (20, 10) within layer
groundLayer.putTileAt(1, 20, 10);
// Put tile index 2 at world pixel location (200, 50) within layer
// (This uses the main camera's coordinate system by default)
groundLayer.putTileAtWorldXY(2, 200, 50);

타일을 얻거나 조작하는 타일 맵 레이어 (및 타일 맵) 메소드는 종종 쌍으로 나타납니다. putTileAt와 같은 한 가지 방법은 타일 그리드 단위에서 작동합니다. (0, 2)는 레이어의 첫 번째 열과 세 번째 행에 해당합니다. putTileAtWorldXY와 같은 다른 방법은 세계 픽셀 단위로 작동하므로 마우스 아래에있는 타일을 찾기 쉽습니다. 타일 그리드 단위에서 세계 픽셀 좌표로 변환하거나 worldToTileXY, tileToWorldXY에서 변환하는 방법도 있습니다.

이 방법을 Phaser 입력과 함께 사용하면 마우스로 레이어에 타일을 그릴 수 있습니다.
function update() {
// Convert the mouse position to world position within the camera
const worldPoint = this.input.activePointer.positionToCamera(this.cameras.main);
// Draw tiles (only within the groundLayer)
if (this.input.manager.activePointer.isDown) {
groundLayer.putTileAtWorldXY(353, worldPoint.x, worldPoint.y);
}
}

다음 예제는이 모든 것을 하나로 모으고 Shift 키를 누른 상태에서 클릭하여 타일을 클릭하고 지우면 타일을 페인트 할 수 있습니다. worldToTileXY 및 tileToWorldXY는 마우스가 현재 타일을 시각화하기 위해 간단한 그래픽 오버레이를 만드는 데 사용됩니다.

참고 : "Edit on CodeSandbox"버튼을 클릭하고 모든 파일을 쉽게 볼 수있는 전체 화면에서 코드를 확인하십시오.

CodeSandBox: https://codesandbox.io/s/31xpvv85om?hidenavigation=1&module=%2Fjs%2Findex.js&moduleview=1

live example: https://www.mikewesthad.com/phaser-3-tilemap-blog-posts/post-2/01-drawing-tiles

source code: https://github.com/mikewesthad/phaser-3-tilemap-blog-posts/blob/master/examples/post-2/01-drawing-tiles


코드 모듈화

개별 타일을 추가하거나 제거하는 것은 매우 쉽습니다. 따라서 복잡성을 높이고 플랫폼 작성자의 기반을 구축해 보겠습니다.


지금까지는 사전로드, 설정 및 업데이트 기능이있는 단일 파일을 사용하여 Phaser를 신속하게 실행 해 보았습니다. 단순한 예제에서는 훌륭하지만, 일단 복잡한 것을 얻으면 악몽이됩니다.

플랫폼 기반을 구축하기 위해 우리는 그 모 놀리 식 파일 구조를 "모듈"이라고 불리는 분리 된 파일로 분리하고자합니다. 코드에 모듈화 된 많은 이유가 있습니다. 잘 사용된다면, 생각하기 쉬운 코드의 이식성 있고 재사용 가능한 덩어리를 만드는 데 도움이됩니다. 모듈에 익숙하지 않다면 Eloquent JavaScript의 모듈 장이나이 개요를 확인하십시오.

최신 브라우저 (2017 년 후반)를 사용하고 있다면, 다음과 같이 HTML에 type = "module"속성을 추가하여 프로젝트에서 모듈을 사용할 수 있습니다 (webpack, parcel 등은 필요하지 않음).

<script src="./js/index.js" type="module"></script>
(참고 : Parcel bundler를 사용하여 모듈 지원을 사용할 수 있기 때문에 CodeSandbox 데모에서는이 내용을 볼 수 없지만이 시리즈의 소스 코드에서 볼 수 있습니다.)

index.js 내부에서 하나 이상의 내보내기가있는 다른 파일에서 함수, 객체 또는 프리미티브 값을 가져올 수 있습니다. 가져 오기 및 내보내기 기능은 단일 파일 코드를 별도의 파일로 분할하는 방법을 제공합니다. 이를 염두에두고, 우리의 새로운 프로젝트 구조는 다음과 같습니다 :

.
├── assets/
|
├── js/
| |
| ├── index.js
| | Creates the Phaser game from our config
| |
| ├── platformer-scene.js
| | The new home of our scene (e.g. preload, create, update)
| |
| └── player.js
| Handles player movement and animations
|
└── index.html
Loads up index.js
index.js부터 시작하여 아래 코드를 확인하십시오. 거기에서 가져 오기가 표시되면 스레드를 참조하기 위해 참조되는 파일을 확인하십시오. 이것은 우리가 다음 섹션을 위해 구축 할 기초가 될 것입니다.

CodeSandBox: https://codesandbox.io/s/p5pqqjk6q0?hidenavigation=1&module=%2Fjs%2Findex.js&moduleview=1

live example: https://www.mikewesthad.com/phaser-3-tilemap-blog-posts/post-2/02-modules-demo

SourceCode: https://github.com/mikewesthad/phaser-3-tilemap-blog-posts/blob/master/examples/post-2/02-modules-demo

여기 모든 코드 줄을 나누는 것은 게시물의 범위에서 벗어나지 만 계속 진행하기 전에 손질 된 player.js 버전을 살펴 보겠습니다.
export default class Player {
constructor(scene, x, y) {
this.scene = scene;
// Create the physics-based sprite that we will move around and animate
this.sprite = scene.physics.add
.sprite(x, y, "player", 0)
.setDrag(1000, 0)
.setMaxVelocity(300, 400)
.setSize(18, 24)
.setOffset(7, 9);
// Set up animations & keyboard input (see CodeSandbox)
}
update() {
// Move sprite & change animation based on keyboard input (see CodeSandbox)
}
destroy() {
this.sprite.destroy();
}
}

이것은 platformer-scene.js에서 플레이어 논리의 대부분을 분리 시켜서 중요한 패턴을 설정합니다. 이 장면은 플레이어가 다른 세계와 어떻게 상호 작용하는지 걱정해야합니다. 적절한 시간에 updateand destroy를 호출하는 한이 플레이어 파일을 완전히 별개의 프로젝트로 가져 와서 작업해도 괜찮습니다.

또한이 클래스가 앞으로 다시 사용할 패턴을 설정한다는 점도 주목할 가치가 있습니다. player.js는 Phaser.GameObjects.Sprite를 확장하지 않는 클래스를 내 보냅니다. 대신 Player 자체가 스프라이트가 아닌 Sprite 속성이있는보다 유연한 구성 요소 패턴을 포함합니다.

이 코드를 더 모듈화하기 위해 할 수있는 일은 훨씬 많습니다 (예 : Phaser의 이벤트 시스템을 활용하거나, samme / phaser-plugin-update를 참조하거나, 모션 로직을 애니메이션과 분리하는 등) 많은 새로운 개념.


The Platformer



이전 섹션의 코드에 대한 두 가지 중요한 타일 맵 관련 확장이 있습니다.
1. 충돌 타일 페인팅
2. 적절한 히트 박스 및 로직과 함께 스파이크 추가

먼저 타일을 페인트합니다. 이것은 이전 코드와 비슷하지만 주름이 추가되어 우리가 추가하는 타일이 충돌하여 플레이어가 착륙 할 수있게하려는 것입니다. putTileAtWorldXY는 조작중인 Tile 객체를 반환합니다. 타일 오브젝트는 매우 간단합니다. 그들은 렌더링 한 타일의 인덱스와 위치를 물리 정보와 함께 보유합니다. tile.setCollision을 사용하여 충돌을 사용할 수 있습니다.
// When mouse is down, put a colliding tile at the mouse location
const pointer = this.input.activePointer;
const worldPoint = pointer.positionToCamera(this.cameras.main);
if (pointer.isDown) {
const tile = groundLayer.putTileAtWorldXY(6, worldPoint.x, worldPoint.y);
tile.setCollision(true);
}
타일에는 다른 유용한 속성 및 메서드가 있습니다. tile.x와 tile.y는 그리드 단위의 위치입니다. tile.getLeft (), tile.getBottom (), tile.getCenterX () 등은 세계 픽셀 단위로 위치를 지정합니다. 자세한 내용은 문서를 확인하십시오.

완벽한 타일을 칠할 수 있습니다. 하지만 스파이크 문제가 있습니다.


Phaser의 아케이드 물리 (AP) 한계 중 하나는 충돌하는 타일의 물리 구조가 타일 너비와 타일 높이와 일치하는 사각형 이어야 한다는 것입니다. 스파이크의 크기는 6px 밖에되지 않지만 32px x 32px의 히트 박스가 제공됩니다. this.groundLayer.renderDebug를 사용하여 지도를 로드하고 충돌하는 타일을 렌더링하면 문제가 더욱 분명 해집니다.



이것을 해결할 수 있는 몇 가지 방법이 있습니다. 우리는 물리엔진을 위해 Matter.js를 사용하는 것으로 전환 할 수 있습니다. 그러나 그것은 과잉입니다. 대신 스파이크를 타일에서 스프라이트로 변환 해 봅시다. 맞춤형 크기의 물리학 체를 제공 할 수 있습니다. (... 타일 맵 API를 더 자세히 살펴볼 수있는 편리한 변명 거리를 제공합니다!)

타일 맵에는 타일 오브젝트와 타일을 스프라이트로 변환하는 메소드가 있습니다. createFromObjects & createFromTiles. 게임 엔티티가 있어야하는 위치를 시각적으로 배치 할 수 있습니다. 적들이 당신의 레벨에 있어야 하는 장소에 타일로 된 물체를 놓은 다음 게임이 부팅 될 때 적절한 스프라이트로 바꿉니다. 다음은 이 전략을 사용하여 Tiled 오브젝트를 움직이는 동전 스프라이트로 대체하는 다이어그램 Phaser 예제입니다.
왼쪽은 브라우저에서 실행중인 Phaser 예제이고 오른쪽은 Tiled입니다. 너비 / 높이 / 뒤집기 등이 움직이는 동전 스프라이트로 복사되는 방법에 유의하십시오.
코드의 컨텍스트에서 스파이크를 스프라이트로 변환하는 것은 좀 더 복잡하므로 ForEachTile을 사용하여 레이어의 모든 Tile 객체를 반복하여 타일 → 스프라이트 논리의 자체 사용자 정의 버전을 롤백 할 수 있습니다.
// platformer-scene.js, inside of setup:
// Create a physics group - useful for colliding the player against all the spikes
this.spikeGroup = this.physics.add.staticGroup();
// Loop over each Tile and replace spikes (tile index 77) with custom sprites
this.groundLayer.forEachTile(tile => {
if (tile.index === 77) {
// A sprite has its origin at the center, so place the sprite at the center of the tile
const x = tile.getCenterX();
const y = tile.getCenterY();
const spike = this.spikeGroup.create(x, y, "spike");
// The map has spike tiles that have been rotated in Tiled ("z" key), so parse out that angle
// to the correct body placement
spike.rotation = tile.rotation;
if (spike.angle === 0) spike.body.setSize(32, 6).setOffset(0, 26);
else if (spike.angle === -90) spike.body.setSize(6, 32).setOffset(26, 0);
else if (spike.angle === 90) spike.body.setSize(6, 32).setOffset(0, 0);
// And lastly, remove the spike tile from the layer
this.groundLayer.removeTileAt(tile.x, tile.y);
}
});

이제 상황과 관련하여 플레이어가 스파이크에 닿으면 게임을 다시 시작하는 모든 작업이 수행됩니다.

CodeSaneBox: https://codesandbox.io/s/mo2j4nvkxy?hidenavigation=1&module=%2Fjs%2Findex.js&moduleview=1

Live Example: https://www.mikewesthad.com/phaser-3-tilemap-blog-posts/post-2/03-drawing-platformer

Source Code: https://github.com/mikewesthad/phaser-3-tilemap-blog-posts/blob/master/examples/post-2/03-drawing-platformer


Up Next

다음 글에서 동적 타일 맵을 사용하여 프로 시저 형 던전을 작성하십시오.

읽어 주셔서 감사합니다. 향후 게시물에서보고 싶은 내용이 있으면 알려 주시기 바랍니다.

댓글 없음:

댓글 쓰기