JavaScript로 Slither.io를 만드는 법 : Part 2 - Snake

이것은 JavaScript 및 Phaser로 Slither.io를 만드는 튜토리얼 시리즈의 두 번째 부분입니다! Part 1을 아직 보지 않았다면 살펴보십시오.
예제를 살펴보고 이 부분의 소스 코드를 살펴보십시오.

Assets 폴더를 엽니다. 이것은 우리의 모든 이미지가 있는 곳입니다. 우리는 이 부분에있는 모든 것을 사용하지 않을 것입니다. 배경에는 tile.png 만 사용하고 뱀 섹션에는 circle.png 만 사용합니다.

이제 index.html을 엽니 다. 다음은 자체 실행 익명 함수에서 게임을 초기화 하는 예제입니다.
(function() {
    var game = new Phaser.Game(800, 500, Phaser.AUTO, null);
    game.state.add('Game', Game);
    game.state.start('Game');
})();
'Game'이라는 상태를 추가하고 Game 함수를 전달한 다음 시작합니다.

src 폴더에서 game.js를 엽니다. 게임 함수는 다음과 같이 정의됩니다.
Game = function(game) {}
Game.prototype = {
   ...
}

게임 프로토 타입에서는 preload 단계로 Assets을 로드합니다.
preload: function() {
    //load assets
   this.game.load.image('circle','asset/circle.png');
   this.game.load.image('background', 'asset/tile.png');
}

생성 단계에서는 월드 경계를 설정하고 배경을 추가하고 P2 물리를 시작하고 뱀을 추가합니다.
create: function() {
    var width = this.game.width;
    var height = this.game.height;

    this.game.world.setBounds(-width, -height, width*2, height*2);
    this.game.stage.backgroundColor = '#444';

    //add tilesprite background
    var background = this.game.add.tileSprite(-width, -height,
        this.game.world.width, this.game.world.height, 'background');

    //initialize physics and groups
    this.game.physics.startSystem(Phaser.Physics.P2JS);

    this.game.snakes = [];

    //create player
    var snake = new Snake(this.game, 'circle', 0, 0);
    this.game.camera.follow(snake.head);
}

우리는 큰 세계를 만들었기 때문에 카메라가 뱀의 머리를 따라 가게합니다. 뱀 객체가 생성되면 this.game.snakes 배열에 추가됩니다. 이 배열은 여러 위치에서 현재의 뱀에 액세스하는 데 편리합니다.

마지막으로 뱀 클래스에서 update 메소드를 사용할 계획이므로 게임의 모든 뱀에 대해 메인 업데이트 루프에서 호출해야 합니다.
update: function() {
    //update game components
    for (var i = this.game.snakes.length - 1 ; i >= 0 ; i--) {
        this.game.snakes[i].update();
    }
}

이제 Snake 클래스를 살펴 보겠습니다.
snake.js를 엽니다. 먼저 게임 객체, 스프라이트 키 및 머리 위치를 매개 변수로 가져 와서 많은 변수를 초기화합니다.
Snake = function(game, spriteKey, x, y) {
    this.game = game;
    //create an array of snakes in the game object and add this snake
    if (!this.game.snakes) {
        this.game.snakes = [];
    }
    this.game.snakes.push(this);
    this.debug = false;
    this.snakeLength = 0;
    this.spriteKey = spriteKey;

    //various quantities that can be changed
    this.scale = 0.6;
    this.fastSpeed = 200;
    this.slowSpeed = 130;
    this.speed = this.slowSpeed;
    this.rotationSpeed = 40;

    //initialize groups and arrays
    this.collisionGroup = this.game.physics.p2.createCollisionGroup();
    this.sections = [];
    //the head path is an array of points that the head of the snake has
    //traveled through
    this.headPath = [];
    this.food = [];

    this.preferredDistance = 17 * this.scale;
    this.queuedSections = 0;

    this.sectionGroup = this.game.add.group();
    //add the head of the snake
    this.head = this.addSectionAtPosition(x,y);
    this.head.name = "head";
    this.head.snake = this;

    this.lastHeadPosition = new Phaser.Point(this.head.body.x, this.head.body.y);
    //add 30 sections behind the head
    this.initSections(30);

    this.onDestroyedCallbacks = [];
    this.onDestroyedContexts = [];
}
가장 중요한 것은 addSectionAtPosition을 호출하여 헤드를 추가하는 것입니다.
initSections를 호출하여 헤드 바로 아래에 30 개의 섹션을 추가합니다. initSections 메서드는 헤드를 만든 후에 한 번만 호출 할 수 있습니다. 나중에 우리는 편리한 메소드 addSectionsAfterLast를 사용하므로 새로운 위치에 대해 걱정할 필요가 없습니다. 이 방법들을 한 번에 하나씩 살펴 보겠습니다.

addSectionAtPosition 메소드를 사용하여 단일 섹션을 추가 할 수 있습니다.
addSectionAtPosition: function(x, y) {
    //initialize a new section
    var sec = this.game.add.sprite(x, y, this.spriteKey);
    this.game.physics.p2.enable(sec, this.debug);
    sec.body.setCollisionGroup(this.collisionGroup);
    sec.body.collides([]);
    sec.body.kinematic = true;

    this.snakeLength++;
    this.sectionGroup.add(sec);
    sec.sendToBack();
    sec.scale.setTo(this.scale);

    this.sections.push(sec);

    //add a circle body to this section
    sec.body.clearShapes();
    sec.body.addCircle(sec.width*0.5);

    return sec;
}

이제 뱀 뒤에 적절한 위치에 섹션을 쉽게 추가 할 수 있어야 합니다. 먼저 initSections 메서드를 사용하여 뱀의 머리 바로 뒤에 섹션을 추가합니다.
initSections: function(num) {
    //create a certain number of sections behind the head
    //only use this once
    for (var i = 1 ; i <= num ; i++) {
        var x = this.head.body.x;
        var y = this.head.body.y + i * this.preferredDistance;
        this.addSectionAtPosition(x, y);
        //add a point to the head path so that the section stays there
        this.headPath.push(new Phaser.Point(x,y));
    }
}

this.headPath는 뱀의 머리가 통과 한 점들의 배열입니다. 처음에는 아무 지점도 통과하지 못했기 때문에 초기 섹션을 추가 한 지점을 추가합니다. 그러나 우리는 또한 뱀의 뒤쪽에 새로운 섹션을 추가하는 편리한 방법이 필요합니다. addSectionsAfterLast 메소드를 사용합니다.
addSectionsAfterLast: function(amount) {
    this.queuedSections += amount;
}
나중에 queuedSections 속성이 0보다 클 때 새 섹션을 추가하는 위치를 알 수 있습니다.

업데이트 함수를 확인하겠습니다. 이것은 메인 업데이트 루프에서 호출되는 메소드입니다. 이 방법으로 우리는 앞으로 뱀을 움직일 것입니다.

헤드의 경로 (headPath 배열)를 사용하여 섹션을 배치 할 위치를 결정합니다. update 메소드가 호출 될 때마다, 배열의 앞부분에 새 머리 위치 인 점을 추가합니다. 섹션 사이에 preferredDistance 번호가 있으므로 나머지 섹션을 경로에 배치 할 위치를 결정할 수 있습니다. 다음은 이를 설명하는 그림입니다.

일반적으로 섹션은 headPath 내에 2 포인트 이상 떨어져 배치되지만 명확한 예제를 위해이 작업을 수행했습니다. 또한 "L"은 매번 계산해야하는 작은 십진수입니다. 우리가 그것들을 더할 때, 그것들은 preferredDistance에 완벽하게 들어 맞지 않을 것입니다, 그래서 우리는 선호하는 것과 가장 가까운 거리를 제공하는 점을 간단히 선택해야 합니다.

이제 update 메소드를 시작하는 방법을 살펴 보겠습니다. 먼저 head를 이동하고 headPath의 마지막 점을 제거한 다음 새로운 머리 위치를 배열의 앞쪽에 배치합니다.
var speed = this.speed;
this.head.body.moveForward(speed);
var point = this.headPath.pop();
point.setTo(this.head.body.x, this.head.body.y);
this.headPath.unshift(point);

다음으로, 헤드 경로를 따라 적절한 지점에 섹션을 배치해야합니다.
var index = 0;
var lastIndex = null;
for (var i = 0 ; i < this.snakeLength ; i++) {

    this.sections[i].body.x = this.headPath[index].x;
    this.sections[i].body.y = this.headPath[index].y;

    //hide sections if they are at the same position
    if (lastIndex && index == lastIndex) {
        this.sections[i].alpha = 0;
    }
    else {
        this.sections[i].alpha = 1;
    }

    lastIndex = index;
    //this finds the index in the head path array that the next point
    //should be at
    index = this.findNextPointIndex(index);
}
우리가 호출 한 findNextPointIndex 메소드는 점의 거리 공식을 사용하여 이전 섹션이 있던 위치를 기준으로 다음 섹션을 배치 할 위치를 확인합니다. 우리는 그 방법을 잠시 살펴볼 것입니다.

다음으로 업데이트 루프에서 배열이 너무 짧으면 headPath에 점을 추가하고 길이가 너무 길면 점을 제거합니다 (섹션을 배치 할 때 배열의 마지막 인덱스에 도달하지 않기 때문에).
//continuously adjust the size of the head path array so that we
//keep only an array of points that we need
if (index >= this.headPath.length - 1) {
    var lastPos = this.headPath[this.headPath.length - 1];
    this.headPath.push(new Phaser.Point(lastPos.x, lastPos.y));
}
else {
    this.headPath.pop();
}

이제 우리는 두 번째 섹션이, 이 메서드의 이전 호출에서 첫 번째 섹션 (head)이 있던 위치에 도달 할 때마다 onCycleComplete 메서드를 호출하려고합니다.
var i = 0;
var found = false;
while (this.headPath[i].x != this.sections[1].body.x &&
this.headPath[i].y != this.sections[1].body.y) {
    if (this.headPath[i].x == this.lastHeadPosition.x &&
    this.headPath[i].y == this.lastHeadPosition.y) {
        found = true;
        break;
    }
    i++;
}
if (!found) {
    this.lastHeadPosition = new Phaser.Point(this.head.body.x, this.head.body.y);
    this.onCycleComplete();
}

하지만 왜 우리가 이것을 필요로 합니까? 마지막에 섹션을 추가 할 때 동시에 섹션을 추가하고 싶지는 않습니다. 모든 섹션이 상당한 거리만큼 앞으로 움직일 때마다 섹션을 추가하려고합니다. 이것이 우리가 queuedSections 변수를 가지는 이유이며, onCycleComplete 메소드에 대기중인 섹션 중 하나를 추가합니다 :
onCycleComplete: function() {
    if (this.queuedSections > 0) {
        var lastSec = this.sections[this.sections.length - 1];
        this.addSectionAtPosition(lastSec.body.x, lastSec.body.y);
        this.queuedSections--;
    }
}

이제 이전에 사용한 findNextPointIndex 메서드를 살펴 보겠습니다.
findNextPointIndex: function(currentIndex) {
    var pt = this.headPath[currentIndex];
    // 우리는 거리 이전에있는 지점으로부터이 
    // 거리만큼 떨어져있는 지점을 찾으려고합니다.     
    //여기서 거리는 두 지점을 연결하는 모든 선의 총 길이입니다
    var prefDist = this.preferredDistance;
    var len = 0;
    var dif = len - prefDist;
    var i = currentIndex;
    var prevDif = null;
    // 이 루프는 함수의 주어진 인덱스에서 시작하는 헤드의 경로상의 점 사이의 
    // 거리를 합한 다음의 합계가 두 개의 뱀 섹션 사이의 선호 거리에 
    // 근접 할 때까지 계속됩니다
    while (i+1 < this.headPath.length && (dif === null || dif < 0)) {
        //get distance between next two points
        var dist = Util.distanceFormula(
            this.headPath[i].x, this.headPath[i].y,
            this.headPath[i+1].x, this.headPath[i+1].y
        );
        len += dist;
        prevDif = dif;
        // 우리는 현재 합계와 선호 거리 사이의 차이를 0에 가깝게하려고합니다.
        dif = len - prefDist;
        i++;
    }

    // 루프가 완료되면 차이를 0에 가깝게 만드는 인덱스를 선택하십시오.
    if (prevDif === null || Math.abs(prevDif) > Math.abs(dif)) {
        return i;
    }
    else {
        return i-1;
    }
}
Util.distanceFormula를 호출하여 점 사이의 거리를 얻은 다음 누적 합계에 추가합니다. 이 합계를 선호 거리와 비교하는 데 사용합니다. 우리는 Util 객체를 곧 살펴볼 것입니다.

뱀의 전체 크기를 변경하고자 하므로 setScale 메서드를 사용합니다.
setScale: function(scale) {
    this.scale = scale;
    this.preferredDistance = 17 * this.scale;

    //scale sections and their bodies
    for (var i = 0 ; i < this.sections.length ; i++) {
        var sec = this.sections[i];
        sec.scale.setTo(this.scale);
        sec.body.data.shapes[0].radius = this.game.physics.p2.pxm(sec.width*0.5);
    }
}

자, 다음과 같이 뱀 크기를 증가시킬 수 있습니다 :
incrementSize: function() {
    this.addSectionsAfterLast(1);
    this.setScale(this.scale * 1.01);
}

우리는 아직 뱀을 파괴하는 것이 없지만 나중에 뱀을 처리해야합니다. 우리 프로그램의 다른 부분은 나중에 뱀이 언제 파괴되는지 알고 싶어하기 때문에 뱀이 파괴 될 때 콜백을 처리해야합니다. 먼저 콜백을 배열하여 콜백을 설정할 수 있습니다.
addDestroyedCallback: function(callback, context) {
    this.onDestroyedCallbacks.push(callback);
    this.onDestroyedContexts.push(context);
}

그런 다음, 우리는 모든 것을 파괴하고 콜백을 다음과 같이 호출 할 수 있습니다.
destroy: function() {
    this.game.snakes.splice(this.game.snakes.indexOf(this), 1);
    this.sections.forEach(function(sec, index) {
        sec.destroy();
    });

    //call this snake's destruction callbacks
    for (var i = 0 ; i < this.onDestroyedCallbacks.length ; i++) {
        if (typeof this.onDestroyedCallbacks[i] == "function") {
            this.onDestroyedCallbacks[i].apply(
                this.onDestroyedContexts[i], [this]);
        }
    }
}

마지막으로 util.js를 살펴보십시오. Util은 유용한 기능을 가진 객체 일뿐입니다. 이 객체에는 거리 공식과 임의의 정수 생성기가 있습니다.
const Util = {
    /**
     * Generate a random number within a closed range
     * @param  {Integer} min Minimum of range
     * @param  {Integer} max Maximum of range
     * @return {Integer}     random number generated
     */
    randomInt: function(min, max) {
        min = Math.ceil(min);
        max = Math.floor(max);
        return Math.floor(Math.random() * (max - min + 1)) + min;
    },
    /**
     * Calculate distance between two points
     * @param  {Number} x1 first point
     * @param  {Number} y1 first point
     * @param  {Number} x2 second point
     * @param  {Number} y2 second point
     */
    distanceFormula: function(x1, y1, x2, y2) {
        var withinRoot = Math.pow(x1-x2,2) + Math.pow(y1-y2,2);
        var dist = Math.pow(withinRoot,0.5);
        return dist;
    }
};

우리의 뱀은 기능적이지만 아직 돌 수 없습니다! 걱정하지 마십시오. 그것은 뱀의 머리를 돌리는 것입니다. 3 부에서 컨트롤을 돌려서 처리 할 것입니다. 그 동안 뱀을 테스트하십시오. 속도를 변경하면 섹션 사이의 거리가 거의 동일하게 유지됩니다. 3 부에서는 뱀을 플레이어의 뱀 또는 봇으로 확장하는 방법을 보여줍니다!


댓글 없음:

댓글 쓰기