크리티컬섹션 :: Phaser Js로 Roguelike 만드는 법

달력

112024  이전 다음

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

Roguelikes는 Dredmor의 Dungeons, Spelunky, Isaac의 바인딩, FTL이 광범위한 잠재 고객에게 도달하고 비평을 받고있는 게임과 함께 최근 주목을 받아 왔습니다. 작은 틈새 시장에서 하드 코어 플레이어가 오래 동안 즐기던 다양한 조합의 rockguelike 요소는 이제 많은 기존 장르에 깊이와 재연성을 가져다줍니다.




이 튜토리얼에서는 JavaScript와 HTML 5 게임 엔진 인 Phaser를 사용하여 전통적인 RogueLike 게임을 만드는 법을 배웁니다. 결국, 당신은 당신의 브라우저에서 재생할 수있는, 완벽한 기능을 갖춘 간단한 RogueLike 게임을 갖게 될 것입니다! (우리의 목적을 위해 전통적인 roguelike는 permadeath와 함께 싱글 플레이어, 무작위, 차례 기반 던전 크롤러로 정의됩니다.)



준비하기

이 자습서에서는 텍스트 편집기와 브라우저가 필요합니다. 나는 Notepad ++를 사용하고 있으며 광범위한 개발자 도구를 위해 Chrome을 선호하지만 워크 플로우는 선택한 텍스트 편집기와 브라우저에서 거의 동일합니다.


그런 다음 소스 파일을 다운로드 하고 init 폴더로 시작해야합니다. 여기에는 게임용 Phaser와 기본 HTML 및 JS 파일이 포함되어 있습니다. 현재 비어있는 rl.js 파일에 게임 코드를 씁니다.


index.html 파일은 단순히 Phaser와 앞서 언급 한 게임 코드 파일을로드합니다.

<!DOCTYPE html>
<head>
    <title>roguelike tutorial</title>
    <script src="phaser.min.js"></script>
    <script src="rl.js"></script>
</head>
</html>



초기화 및 정의

당분간은 ASCII 그래픽을 사용하게 될 것입니다. 미래에는 비트 맵 그래픽으로 바꿀 수 있지만, 지금은 간단한 ASCII를 사용하면 더 쉽게 사용할 수 있습니다.


폰트 크기, 맵의 크기 (즉, 레벨), 그리고 스폰자의 수에 대한 몇 가지 상수를 정의 해 보겠습니다.

// font size
var FONT = 32;
 
// map dimensions
var ROWS = 10;
var COLS = 15;
 
// number of actors per level, including player
var ACTORS = 10;



Phaser를 초기화하고 키보드 키 업 이벤트를 설정해 봅시다. 

// initialize phaser, call create() once done
var game = new Phaser.Game(COLS * FONT * 0.6, ROWS * FONT, Phaser.AUTO, null, {
        create: create
});
 
function create() {
        // init keyboard commands
        game.input.keyboard.addCallbacks(null, null, onKeyUp);
}
 
function onKeyUp(event) {
        switch (event.keyCode) {
                case Keyboard.LEFT:
 
                case Keyboard.RIGHT:
 
                case Keyboard.UP:
 
                case Keyboard.DOWN:
 
        }
}



기본 모노 스페이스 글꼴은 높이가 60 % 정도되는 경향이 있으므로 캔버스 크기를 0.6 * 글꼴 크기 * 열 수로 초기화했습니다. 우리는 또한 Phaser에게 초기화가 끝난 직후에 create () 함수를 호출해야한다고 말하면서 키보드 컨트롤을 초기화합니다.


볼거리가 많지는 않지만 여기까지 게임을 볼 수 있습니다!



지도

타일 맵은 우리의 플레이 영역을 나타냅니다. 

// the structure of the map
var map;



가장 단순한 형태의 절차를 사용하여 맵을 생성합니다. 벽을 포함 할 셀과 바닥을 임의로 결정합니다.

function initMap() {
        // create a new random map
        map = [];
        for (var y = 0; y < ROWS; y++) {
                var newRow = [];
                for (var x = 0; x < COLS; x++) {
                     if (Math.random() > 0.8)
                        newRow.push('#');
                    else
                        newRow.push('.');
                }
                map.push(newRow);
        }
}



관련 게시물

- BSP 트리를 사용하여 게임 맵을 생성하는 방법

- 셀 오토마타를 사용하여 무작위 동굴 생성


이것은 우리에게 80%가 벽이고 나머지가 바닥 인것을 제공합니다.


키보드 이벤트 리스너를 설정 한 직후 create () 함수로 게임의 새 맵을 초기화합니다.

function create() {
        // init keyboard commands
        game.input.keyboard.addCallbacks(null, null, onKeyUp);
 
        // initialize map
        initMap();
}


여기서 데모를 볼 수 있습니다. 아직 지도를 렌더링하지 않았으므로 아무 것도 볼 수 없습니다.



화면

지도를 그릴 시간입니다! 우리의 화면은 각각 하나의 문자를 포함하는 텍스트 요소의 2D 배열입니다.

// the ascii display, as a 2d array of characters
var asciidisplay;


두 가지 모두 간단한 ASCII 문자이기 때문에 지도를 그리면 화면의 내용이 지도의 값으로 채워집니다.

function drawMap() {
    for (var y = 0; y < ROWS; y++)
        for (var x = 0; x < COLS; x++)
            asciidisplay[y][x].content = map[y][x];
}



마지막으로 맵을 그리기 전에 화면을 초기화해야합니다. 우리는 create () 함수로 돌아 갑니다.

function create() {
        // init keyboard commands
        game.input.keyboard.addCallbacks(null, null, onKeyUp);
 
        // initialize map
        initMap();
 
        // initialize screen
        asciidisplay = [];
        for (var y = 0; y < ROWS; y++) {
                var newRow = [];
                asciidisplay.push(newRow);
                for (var x = 0; x < COLS; x++)
                        newRow.push( initCell('', x, y) );
        }
        drawMap();
}
 
function initCell(chr, x, y) {
        // add a single cell in a given position to the ascii display
        var style = { font: FONT + "px monospace", fill:"#fff"};
        return game.add.text(FONT*0.6*x, FONT*y, chr, style);
}


이제 프로젝트를 실행할 때 무작위지도가 표시됩니다.




액터

다음 줄에는 플레이어 캐릭터와 패배해야 할 적들이 있습니다. 각 액터는 맵의 위치에 대해 x와 y, 히트 포인트에 대해 hp의 세 필드가있는 객체입니다.


우리는 모든 액터를 actorList 배열에 유지합니다 (첫 번째 요소는 플레이어입니다). 우리는 또한 빠른 검색을위한 키로 액터의 위치와 연관 배열을 유지하므로 어떤 액터가 특정 위치를 차지하고 있는지 찾기 위해 전체 액터리스트를 반복 할 필요가 없습니다. 이것은 우리가 이동과 전투를 코드화 할 때 우리를 도울 것입니다.

// a list of all actors; 0 is the player
var player;
var actorList;
var livingEnemies;
 
// points to each actor in its position, for quick searching
var actorMap;


우리는 모든 액터를 만들고 지도에 무작위로 자유로운 위치에 할당합니다.

function randomInt(max) {
   return Math.floor(Math.random() * max);
}
 
function initActors() {
        // create actors at random locations
        actorList = [];
        actorMap = {};
        for (var e=0; e<ACTORS; e++) {
                // create new actor
                var actor = { x:0, y:0, hp:e == 0?3:1 };
                do {
                        // pick a random position that is both a floor and not occupied
                        actor.y=randomInt(ROWS);
                        actor.x=randomInt(COLS);
                } while ( map[actor.y][actor.x] == '#' || actorMap[actor.y + "_" + actor.x] != null );
 
                // add references to the actor to the actors list & map
                actorMap[actor.y + "_" + actor.x]= actor;
                actorList.push(actor);
        }
 
        // the player is the first actor in the list
        player = actorList[0];
        livingEnemies = ACTORS-1;
}


액터를 보여줄 시간입니다! 우리는 모든 적을 e로, 플레이어 캐릭터를 HP의 숫자로 그립니다.

function drawActors() {
        for (var a in actorList) {
                if (actorList[a].hp > 0)
                        asciidisplay[actorList[a].y][actorList[a].x].content = a == 0?''+player.hp:'e';
        }
}


방금 작성한 함수를 사용하여 create () 함수에서 모든 액터를 초기화하고 그립니다.

function create() {
    ...
    // initialize actors
    initActors();
    ...
    drawActors();

}.


우리는 이제 플레이어 캐릭터와 적들이 화면에 표시되어 있는것을 볼수 있습니다.




차단 및 이동식 타일

우리는 액터가 스크린과 벽을 벗어나지 않도록해야합니다. 그래서 간단한 체크를 추가하여 주어진 액터가 어느 방향으로 걸어 갈 수 있는지 살펴 봅시다.

function canGo(actor,dir) {
    return  actor.x+dir.x >= 0 &&
        actor.x+dir.x <= COLS - 1 &&
                actor.y+dir.y >= 0 &&
        actor.y+dir.y <= ROWS - 1 &&
        map[actor.y+dir.y][actor.x +dir.x] == '.';
}



이동과 전투

우리는 마침내 약간의 상호 작용에 도달했습니다 : 

이동과 전투! 클래식 roguelikes에서 기본적인 공격은 다른 액터로 이동함으로써 트리거 되기 때문에 우리는 액터와 방향을 취하는 moveTo () 함수 (두 방향을 x와 y를 액터가 들어가는 위치로 옮깁니다).를 추가 하겠습니다.

function moveTo(actor, dir) {
        // check if actor can move in the given direction
        if (!canGo(actor,dir))
                return false;
 
        // moves actor to the new location
        var newKey = (actor.y + dir.y) +'_' + (actor.x + dir.x);
        // if the destination tile has an actor in it
        if (actorMap[newKey] != null) {
                //decrement hitpoints of the actor at the destination tile
                var victim = actorMap[newKey];
                victim.hp--;
 
                // if it's dead remove its reference
                if (victim.hp == 0) {
                        actorMap[newKey]= null;
                        actorList[actorList.indexOf(victim)]=null;
                        if(victim!=player) {
                                livingEnemies--;
                                if (livingEnemies == 0) {
                                        // victory message
                                        var victory = game.add.text(game.world.centerX, game.world.centerY, 'Victory!\nCtrl+r to restart', { fill : '#2e2', align: "center" } );
                                        victory.anchor.setTo(0.5,0.5);
                                }
                        }
                }
        } else {
                // remove reference to the actor's old position
                actorMap[actor.y + '_' + actor.x]= null;
 
                // update position
                actor.y+=dir.y;
                actor.x+=dir.x;
 
                // add reference to the actor's new position
                actorMap[actor.y + '_' + actor.x]=actor;
        }
        return true;
}



기본적으로

- 액터가 올바른 위치로 이동하려고하는지 확인합니다.

- 그 위치에 다른 액터가 있다면, 우리는 그것을 공격합니다 (HP 숫자가 0이되면 죽이기도합니다).

- 새로운 위치에 다른 액터가 없다면, 우리는 거기로 이동합니다.


우리는 또한 마지막 적을 살해 한 후 간단한 승리 메시지를 보여 주고, 우리가 유효한 이동을 수행했는지 여부에 따라 false 또는 true를 반환합니다.


이제는 onKeyUp () 함수로 돌아가서 사용자가 키를 누를 때마다 화면에서 이전 액터의 위치를 지우고 (지도를 위에 그려서) 플레이어 캐릭터를 이동하고, 위치를 찾은 다음 액터를 다시 그립니다.

function onKeyUp(event) {
        // draw map to overwrite previous actors positions
        drawMap();
 
        // act on player input
        var acted = false;
        switch (event.keyCode) {
                case Phaser.Keyboard.LEFT:
                        acted = moveTo(player, {x:-1, y:0});
                        break;
 
                case Phaser.Keyboard.RIGHT:
                        acted = moveTo(player,{x:1, y:0});
                        break;
 
                case Phaser.Keyboard.UP:
                        acted = moveTo(player, {x:0, y:-1});
                        break;
 
                case Phaser.Keyboard.DOWN:
                        acted = moveTo(player, {x:0, y:1});
                        break;
        }
 
        // draw actors in new positions
        drawActors();
}


우리는 곧 행동 변수를 사용하여 각 플레이어가 입력 한 후에 적들이 행동해야 하는지를 알 수 있습니다.




기본 인공 지능


이제 플레이어 캐릭터가 움직이고 공격하기 때문에 플레이어가 적과 거리가 6 단계 이하인 경우 매우 간단한 경로 찾기에 따라 적이 플레이를 공격하도록 하겠습니다.. (플레이어가 멀어지면 적들은 무작위로 걷습니다.)


우리의 공격 코드는 액터가 누구를 공격하는지 상관하지 않습니다. 즉, 만약 당신이 그들을 바로 정렬한다면, 적들은 플레이어 캐릭터를서로 공격 할 것입니다!

function aiAct(actor) {
        var directions = [ { x: -1, y:0 }, { x:1, y:0 }, { x:0, y: -1 }, { x:0, y:1 } ];
        var dx = player.x - actor.x;
        var dy = player.y - actor.y;
 
        // if player is far away, walk randomly
        if (Math.abs(dx) + Math.abs(dy) > 6)
                // try to walk in random directions until you succeed once
                while (!moveTo(actor, directions[randomInt(directions.length)])) { };
 
        // otherwise walk towards player
        if (Math.abs(dx) > Math.abs(dy)) {
                if (dx < 0) {
                        // left
                        moveTo(actor, directions[0]);
                } else {
                        // right
                        moveTo(actor, directions[1]);
                }
        } else {
                if (dy < 0) {
                        // up
                        moveTo(actor, directions[2]);
                } else {
                        // down
                        moveTo(actor, directions[3]);
                }
        }
        if (player.hp < 1) {
                // game over message
                var gameOver = game.add.text(game.world.centerX, game.world.centerY, 'Game Over\nCtrl+r to restart', { fill : '#e22', align: "center" } );
                gameOver.anchor.setTo(0.5,0.5);
        }
}


또한 우리는 적의 한 명이 플레이어를 죽이면 게임오버 메세지를 출력하도록 하였습니다.


이제는 플레이어가 움직일 때마다 적이 움직니는 것처럼  만드는 것입니다. 이 작업은 새로운 위치에 액터를 그리기 전에 onKeyUp () 함수의 끝 부분에 다음 코드를 추가해야합니다.

function onKeyUp(event) {
        ...
        // enemies act every time the player does
        if (acted)
                for (var enemy in actorList) {
                        // skip the player
                        if(enemy==0)
                                continue;
 
                        var e = actorList[enemy];
                        if (e != null)
                                aiAct(e);
                }
 
        // draw actors in new positions
        drawActors();
}




마지막 코드까지 수정된 데모 버젼입니다.


Posted by 마스터킹
|