Fork me on GitHub

스페이스 인베이더스 튜토리얼

이 튜토리얼에서, 우리는 스페이스 인베이더스 클론을 생성할 것입니다. 이 튜토리얼은 주로 코드를 통해 그리고 MelonJS가 제공하는 다른 API들을 사용해서 플랫포머 튜토리얼이 다루지 않는 1게임 요소를 생성하는 것에 초점을 맞출 것입니다.

도입

이 튜토리얼을 진행하시려면, 다음의 것들이 필요합니다:

  • 우리가 튜토리얼에서 기본 템플릿 프로젝트로 사용할 melonJS 보일러 플레이트(boilerplate)
  • 튜토리얼 이미지 파일들, 압축 해제 후 보일러 플레이트 데이터 디렉토리에 두십시오. 이 폴더에는 다음과 같은 것들이 있어야 합니다:
  • data/img/player.png
    data/img/ships.png
    js/game.js
    etc
    
  • melonJS 라이브러리. 만약 보일러 플레이트를 다운로드하셨다면, 아마 이것을 가지고 계실 것입니다. 그것은 /lib 디렉토리 아래에 복사되어야 합니다. 보일러 플레이트는 최소화 태스크를 제공하기에 당신은 개발 버전을 복사할 수 있습니다.
  • 더 많은 정보를 보시려면 melonJS 문서

테스트/디버그 :

당신에게 최선의 방법은 melonJS 보일러 플레이트 도움말에 상세히 나온 것처럼 로컬 웹 서버를 이용하는 것으로, `grunt connect` 툴을 사용함으로써, 그것은 당신이 당신의 게임을 당신의 브라우저에서 http://localhost:8000 url을 사용해 테스트할 수 있도록 할 것입니다.

만약 당신이 파일 시스템을 사용하기를 원하신다면, 문제는 당신이 "교차-원본 요청(cross-origin request)" 보안 오류를 경험하게 될 것입니다. 크롬을 쓰신다면, 당신은 브라우저를 실행하실 때, "--disable-web-security" 파라미터를 사용하시거나 "--allow-file-access-from-files"를 사용하셔야 합니다. 이것은 특정 로컬 컨텐츠를 테스트 하려면 반드시 수행되어야 합니다. 그렇지 않으면 브라우저는 자산을 XHR을 통해 로드할 때 오류를 일으킬 것입니다. 비록 이 방법이 추천되는 방법은 아니지만, 당신이 옵션을 실행하는 한, 당신은 당신의 환경에 보안 취약성을 더하게 됩니다.

우리 배 설정하기

보일러 플레이트로부터 당신의 디렉토리 구조는 이것처럼 보일 것입니다:

data/
  img/
    player.png
    ships.png
js/
  game.js
  resources.js
  entities/
    HUD.js
    entities.js
  screens/
    play.js
    title.js
index.html
index.css

보일러 플레이트는 또한 기본 코드들을 제공합니다. 본 튜토리얼을 위해서는 우리가 필요로 하지 않을 몇몇 파일들이 있습니다. 당신은 screens/title.js 파일과, js/entities 폴더 전체를 지우실 수 있습니다. 그리고 index.html 파일을 업데이트하면 더 이상 이들을 포함하지 않고, game.js 파일에서 타이틀 화면의 레퍼런스를 제거하게 됩니다.

var game = {
    // Run on page load.
    onload : function () {
        // Initialize the video.
        if (!me.video.init(640, 480, {wrapper : "screen", scale : 'auto'})) {
            alert("Your browser does not support HTML5 canvas.");
            return;
        }

        // add "#debug" to the URL to enable the debug Panel
        if (me.game.HASH.debug === true) {
            window.onReady(function () {
                me.plugin.register.defer(this, me.debug.Panel, "debug", me.input.KEY.V);
            });
        }

        // Initialize the audio.
        me.audio.init("mp3,ogg");

        // Set a callback to run when loading is complete.
        me.loader.onload = this.loaded.bind(this);

        // Load the resources.
        me.loader.preload(game.resources);

        // Initialize melonJS and display a loading screen.
        me.state.change(me.state.LOADING);
    },

    // Run on game resources loaded.
    loaded : function () {
        // set the "Play/Ingame" Screen Object
        this.playScreen = new game.PlayScreen();
        me.state.set(me.state.PLAY, this.playScreen);

        // start the game
        me.state.change(me.state.PLAY);
    }
};

Game.js은 게임이 부트스트랩된 곳입니다. 윈도우에서 준비된 이벤트의 모든 js 파일을 로드한 후 index.html는 game.onload 함수를 호출합니다. me.video.init 비트는 캔버스 태그를 생성하고 비디오 설정을 가져옵니다.

12-16행은 디버그 패널을 사용하기 위한 것입니다.

그리고 우리는 이 게임을 어떤 포맷으로 지원할 것인지 말하면서 오디오 엔진을 초기화합니다.

우리는 우리의 로드된 함수에 me.loader에 대한 콜벡을 설정하고, 배열을 통해 어떤 자산이 로드되어야 하는지 말합니다.

이 프로세스의 마지막 단계는 게임의 상태를 로드 상태로 설정하는 것입니다.

loaded : function () {
  this.playScreen = new game.PlayScreen();
  me.state.set(me.state.PLAY, this.playScreen);

  // start the game
  me.state.change(me.state.PLAY);
}

로드된 함수는 이후 플레이 화면을 설정하고 게임에게 플레이 상태를 위한 화면 오브젝트를 사용한다고 말합니다.

그러면 게임 상태는 PLAY 상태가 됩니다.

스페이스 인베이더스로 돌아가기

먼저 추가해야할 것은 이미지들을 resources.js 파일로 넣는 것입니다.

game.resources = [
    { name: "player", type: "image", src: "data/img/player.png" },
    { name: "ships", type: "image", src: "data/img/ships.png" }
];

이 변수는 game.js의 me.loader.preload에게 전달된 것입니다.

자산의 구조는 :

이름 자산의 이름은 당신이 넣고자 하는 것을 사용할 수 있습니다. 스트링 키입니다.
유형 자산의 유형. 유효한 유형들은: audio, binary, image, json, tmx, tsx. 바이너리가 로우 텍스트를 불러오거나 다른 리스트에 없는 포맷을 로드하기 위해서는 좋은 해결책입니다. TMX & TSX는 타일드 파일 포맷입니다. 그들이 xml 이든 json 포맷이든 간에.
src 자산의 경로로, index.html로부터 상대적입니다. 오디오를 위해서는 당신은 직접적인 경로 대신 폴더를 지정해야 합니다.

js/screens/play.js을 열고 두 메소드로부터 코드를 비우세요: onResetEvent와 onDestroyEvent. 그리고 저장하시고, 당신의 웹 브라우저에서 게임을 열어보세요.

아직 볼 수 있는 것이 많지 않습니다. 이것을 바꿉시다.

먼저 플레이어 엔티티를 생성합시다.

js 폴더 아래에 새 파일을 추가하고, player.js라고 부릅니다. index.html 파일 안에 추가하는 것을 확인하세요.

game.Player = me.Sprite.extend({
  init : function () {
      var image = me.loader.getImage("player");
      this._super(me.Sprite, "init", [
          me.game.viewport.width / 2 - image.width / 2,
          me.game.viewport.height - image.height - 20,
          { image : image }
      ]);
  }
});

그래서 우리가 하고 있는 것은 me.Sprite를 확장할 window.game 오브젝트에 함수를 추가하는 것입니다. 그것은 플레이어 이미지를 잡을 init 메소드를 설정합니다.

x 조정을 위해서는, 우리는 정확한 중심을 잡아야 하고, 절반의 배를 빼야, 가운데에 위치할 수 있습니다. 그리고 그것의 set its y 20 픽셀 위로 설정합니다. 그리고 마침내 이미지 인스턴스를 그것에 전달합니다.

this._super를 호출하는 것은 우리가 모 클래스의 메소드를 참조하는 방법입니다. 이 경우, 우리는 me.Sprite 구축기를 호출하고 있습니다.

엔티티 풀에 플레이어를 설정합시다. game.js를 열고 열린 메소드의 위에 다음을 추가합시다:

me.pool.register("player", game.Player);

이제 js/screens/play.js를 열고, onResetEvent 메소드를 편집해 이렇게 보이도록 합시다:

game.PlayScreen = me.ScreenObject.extend({
  /**
   * action to perform on state change
   */
  onResetEvent : function () {
      me.game.world.addChild(me.pool.pull("player"));
  },

  /**
   * action to perform when leaving this screen (state change)
   */
  onDestroyEvent : function () {
  }
});

onResetEvent이벤트는 이 상태가 로드되었을 때 호출됩니다. 그래서 다음을 적용할 때


    me.state.change(me.state.PLAY);
    

game.js 파일 안에, onResetEvent는 호출됩니다.

와! 배가 화면 바닥에 있습니다!

그러나 우리는 아직 로딩 바를 볼 수 있고, 그것 멋지지 않습니다. 이 이유는 MelonJS가 할 필요가 없는 동작을 하고 싶어하지 않기 때문입니다. 때때로 당신은 다시 그려진 배경 이미지를 가지고 있을 것이고, 그것이 원래 로딩 바를 가립니다. 그러나, 우리는 이 게임에서 배경 이미지를 가지고 있지 않기에, 우리가 해야할 것은 컬러 레이어를 추가하는 것입니다.

    me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
    

저것을 플레이 화면에 추가하고 라인 위가 우리가 플레이어를 추가할 곳입니다. 첫 번째 파라미터는 단순히 레이어의 이름이어서, 만약 당신이 필요하다면 게임 세상으로부터 쉽게 불러올 수 있습니다.

두 번째 파라미터는 육각형에서 그릴 색입니다.

두 번째 파라미터는 addChild 함수로 전달되어 z 인덱스가 됩니다. 우리는 그것을 먼저 그리고 싶으므로, 0으로 설정합니다.

이제 귀찮은 로딩 바가 사라졌을 것입니다. 적을 더할 시간입니다. js 폴더 안에 enemy.js라는 새로운 파일을 만들고, 그것을 index.html 파일에 추가합시다.

적들이 플레이어의 레이저와 같은 것들에 충돌할 것이기에, 그것은 me.Sprite와 반대로 me.Entity를 확장해야 하므로, 그것을 해 봅시다:

game.Enemy = me.Entity.extend({
  init: function (x, y) {
      this._super(me.Entity, "init", [x, y, {
          image : "ships",
          width : 32,
          height : 32
      }]);
  },

  update: function (dt) {
      this._super(me.Entity, "update", [dt]);
      return true;
  }
});

적과 함께, 우리는 그들을 다양한 지점에 배치할 필요가 있으므로, x & y는 구축기로 추가될 것이고, me.Entity의 구축기로 보내질 것입니다. 배열 안의 3번째 파라미터는 설정의 해시 값입니다. 설정은 이미지를 game.resources array를 참조해 "배들"로 정의할 것입니다. 너비와 높이는 32x32로 설정됩니다.

우리는 맞춤형 업데이트 메소드를 만들고 있고, 게임 엔진에게 다시 그리라고 말할 것입니다. melon이 게임 루프를 따라갈 때, 이는 결과 상에 or를 할 것입니다. 만약 주어진 프레임 상에 변화가 없다면, 이는 다시 그리지 않을 것입니다. True를 출력하고, 수퍼 메소드를 출력해 적들이 정말로 표현되는 것을 확인할 것입니다.

game.js에서, 적을 엔티티 풀에 추가합시다:

me.pool.register("enemy", game.Enemy);
    

play.js로 돌아가, 적을 게임 세계에 추가합니다. 당신의 play.js는 이렇게 보일 것입니다:

game.PlayScreen = me.ScreenObject.extend({
  /**
   * action to perform on state change
   */
  onResetEvent : function () {
      me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
      me.game.world.addChild(me.pool.pull("player"), 1);
      me.game.world.addChild(me.pool.pull("enemy", 50, 50), 2);
  },

  /**
   * action to perform when leaving this screen (state change)
   */
  onDestroyEvent : function () {
  }
});

시도해보기 위해, 당신은 적을 어떤 x & y에도 놓을 수 있습니다. 저장 & 당신의 브라우저에서 새로고침해 봅시다.

당신은 아마 배들의 모습이 계속 변하는 것을 알아챌 것입니다. 만약 당신이 data/img 폴더의 ships.png 파일을 연다면, 당신은 그것이 4가지 다른 배를 포함한 스프라이트 시트라는 것을 알 것입니다. 그 렌더러블을 위한 me.Entity는 me.AnimationSheet 클래스를 사용합니다. 우리가 렌더러블 속성을 애니메이션을 추가하거나 설정하지 않았기 때문에, 그것은 각 & 모든 프레임을 통해 루프를 도는 것입니다. 그것을 고쳐봅시다.

우리의 적에 새 메소드를 추가합니다:

chooseShipImage: function () {
    var frame = ~~(Math.random() * 3);
    this.renderable.addAnimation("idle", [frame], 1);
    this.renderable.setCurrentAnimation("idle");
}

첫 줄은 단순히 우리가 원하는 프레임을 랜덤화합니다. 배는 32x32이고, 이미지는 64x64이기에, 우리는 4개의 프레임을 가집니다. 숫자가 0 또는 양수일 때 ~~는 Math.floor의 바로가기입니다. 음수에서는, 그것은 Math.ceil처럼 작동합니다.

두 번째 줄은 애니메이션 시트 인스턴스(this.renderable)에 접근하고, 가동되지 않는 새 프레임을 추가하기 위해 addAnimation 함수를 사용합니다. 그래서 우리는 단순히 랜덤으로 생성되는 인덱스를 정의하는 것입니다.

마지막 줄에서, 우리는 현재 애니메이션을 미 가동으로 선택합니다.

이제 구축기 아래에서 함수를 다음과 같이 호출합니다:

game.Enemy = me.Entity.extend({
    init: function (x, y) {
        this._super(me.Entity, "init", [x, y, {
            image: "ships",
            width: 32,
            height: 32
        }]);
        this.chooseShipImage();
    },

    chooseShipImage: function () {
        var frame = ~~(Math.random() * 3);
        this.renderable.addAnimation("idle", [frame], 1);
        this.renderable.setCurrentAnimation("idle");
    },
});

이제 페이지를 새로 고치면, 우리의 배는 그들 중 하나만 뜨게 됩니다. 변화하는 것을 보려면 여러 번 새로고침 해 보세요.

움직임 적용

이제 우리가 배를 화면에 띄웠으므로, 실제로 상호작용을 하도록 해 봅시다.

play.js로 돌아가, 몇몇 키 결합을 추가합시다:

game.PlayScreen = me.ScreenObject.extend({
  /**
   * action to perform on state change
   */
  onResetEvent : function () {
      me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
      me.game.world.addChild(me.pool.pull("player"), 1);
      me.game.world.addChild(me.pool.pull("enemy", 50, 50), 2);

      me.input.bindKey(me.input.KEY.LEFT, "left");
      me.input.bindKey(me.input.KEY.RIGHT, "right");
      me.input.bindKey(me.input.KEY.A, "left");
      me.input.bindKey(me.input.KEY.D, "right");
  },

  /**
   * action to perform when leaving this screen (state change)
   */
  onDestroyEvent : function () {
      me.input.unbindKey(me.input.KEY.LEFT);
      me.input.unbindKey(me.input.KEY.RIGHT);
      me.input.unbindKey(me.input.KEY.A);
      me.input.unbindKey(me.input.KEY.D);
  }
});

여기의 메소드 호출은 매우 직설적입니다. 우리는 키 누르는 것을 묶어 한 액션의 이름으로 만듭니다. 여러 키들이 하나의 액션 이름으로 설정될 수 있습니다.

다양한 키 묶음을 제공하는 것은 좋은 게임 디자인 방법입니다. 그것을 구성할 수 있기 한다면 훨씬 더 좋습니다. 당신은 항상 왼손잡이와 오른손잡이가 다른 설정을 선호한다는 것을 알아둘 필요가 있습니다.

당신은 제가 addChild 호출에 z index 옵션을 추가했다는 것을 알아챘을 지도 모릅니다. 당신이 당신의 그리는 순서를 확인하기에 매우 좋은 방법입니다.

onDestroyEvent는 상태가 변화할 때 이벤트를 제거합니다. 우리는 로드 이후 플레이 상태만을 가지기에 실제로 필요한 것은 아닙니다만 알아두면 좋은 부분입니다.

우리가 이제 키 묶음을 가지고 있으니, 플레이어 움직임을 실행해 봅시다. 다음 업데이트 함수를 플레이어 클래스에 추가합시다:

그리고 init 메소드 상에 플레이어에 화면 상 가장 멀리 갈 수 있는 x 위치(maxX)와 x 속도 속성을 추가합시다:

init : function () {
    var image = me.loader.getImage("player");
    this._super(me.Sprite, "init", [
        me.game.viewport.width / 2 - image.width / 2,
        me.game.viewport.height - image.height - 20,
        image
    ]);
    this.velx = 450;
    this.maxX = me.game.viewport.width - this.width;
},

그리고 업데이트 메소드를 수정해 핵심 이벤트를 체크하고, 플레이어를 적절하게 움직입니다.

update : function (time) {
    this._super(me.Sprite, "update", [time]);
    if (me.input.isKeyPressed("left")) {
        this.pos.x -= this.velx * time / 1000;
    }

    if (me.input.isKeyPressed("right")) {
        this.pos.x += this.velx * time / 1000;
    }

    this.pos.x = this.pos.x.clamp(0, this.maxX);

    return true;
}

우리 게임 오브젝트의 업데이트 함수들은 항상 델타 시간을 받을 것입니다 (밀리초 단위). 그것을 우리 모 클래서 업데이트로 가져가는 것은 중요합니다.

this._super(me.Sprite, "update", [time]);

그 다음으로, 왼쪽 행동이 현재 진행 중인지를 확인하는 문제가 있습니다. 이전에 설정한 속도 값을 사용해, 우리는 단순히 속도 값을 빼서, 초당 델타 값을 곱해줍니다.

if (me.input.isKeyPressed("left")) {
    this.pos.x -= this.velx * time / 1000;
}

오른쪽으로 움직이기 위해, 우리는 오른쪽 액션을 확인하고, 우리의 x 위치에 속도 값을 더해줄 것입니다.

if (me.input.isKeyPressed("right")) {
    this.pos.x += this.velx * time / 1000;
}

우리는 다음으로 clamp를 사용해 x 값이 화면 밖으로 나가지 않는다는 것을 확인할 수 있습니다.

this.pos.x = this.pos.x.clamp(0, this.maxX);

결과 값은 melon에게 다시 그리는 것이 필요한 지를 말해줍니다. 이것은 애니메이션 시트가 주어진 프레임에서 움직여야 함을 지시할 때 유용할 수 있습니다. 그러나, 이것은 단일한 스프라이트이므로, 우리는 그냥 다시 그리라고 말할 수 있습니다.

return true;

파일 저장 & 브라우저 새로고침. A/D 또는 왼쪽 & 오른쪽 화살표 키를 눌러 움직여 봅시다.

적의 움직임

스페이스 인베이더의 정의상 특징은 모든 배들이 한 방향으로, 아래로 움직이고 그 다음에 다른 방향으로 움직인다는 것입니다. 그들은 모두 함께 움직입니다. 우리는 속도 로직을 취해 플레이어에 이용하고 이를 적 클래스에도 적용할 수 있습니다. 그러나 우리는 MelonJS가 우리를 위해 이것을 할 수 있도록 할 수도 있습니다. me.Container 하위 클래스를 사용할 시간입니다

컨테이너 내부의 오브젝트는 그의 모체에 상대적입니다. 그러므로 우리가 컨테이너를 움직이면, 그 안의 모든 오브젝트들이 함께 움직입니다. 이는 회전 & 규모 동작에도 적용됩니다. 그럼 하나를 만들어 봅시다.

파일 생성: js/enemy_manager.js 후 index.html에 추가.

game.EnemyManager = me.Container.extend({
  init : function () {
      this._super(me.Container, "init", [0, 32,
          this.COLS * 64 - 32,
          this.ROWS * 64 - 32
      ]);
      this.COLS = 9;
      this.ROWS = 4;
      this.vel = 16;
  }
});

본질적으로 우리가 여기서 설정하고 있는 것은 시작 위치와 기본 너비입니다. 컨테이너를 32 픽셀 아래로 시작하고 0(또는 x)를 남깁니다.

우리는 배의 너비 & 높이마다 64 픽셀을 할당하고 있습니다. 그리고 마지막 행 & 열이 측면의 여분 공간을 필요로 하지 않으므로 32 픽셀을 뺍니다.

우리의 컨테이너에 적들을 추가할 때, 우리는 다른 메소드가 필요합니다:

createEnemies : function () {
  for (var i = 0; i < this.COLS; i++) {
      for (var j = 0; j < this.ROWS; j++) {
          this.addChild(me.pool.pull("enemy", i * 64, j * 64));
      }
  }
  this.updateChildBounds();
}

9개의 행과, 4개의 열을 생성: 36 척의 배. 우리의 오브젝트 컨테이너가 모든 추가된 종속 항목들에 대해 적절하게 크기 조정되었는지를 확인할 "updateChildBounds" 함수를 호출한다는 것도 주의합시다. 이에 맞추어 우리는 나중에 종속 항목을 제거할 때 똑같은 것을 하게될 것입니다.

이제 play.js에서, 적을 위한 addChild를 제거하고, 적 관리자의 속성을 설정합시다. 아래가 createEnemies를 적용하는 것이고, 그것을 게임 세계에 추가할 것입니다.

onResetEvent : function () {
    me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
    me.game.world.addChild(me.pool.pull("player"), 1);

    this.enemyManager = new game.EnemyManager();
    this.enemyManager.createEnemies();
    me.game.world.addChild(this.enemyManager, 2);

    me.input.bindKey(me.input.KEY.LEFT, "left");
    me.input.bindKey(me.input.KEY.RIGHT, "right");
    me.input.bindKey(me.input.KEY.A, "left");
    me.input.bindKey(me.input.KEY.D, "right");
    me.input.bindKey(me.input.KEY.SPACE, "shoot", true);
}

일단 당신이 저장하고 새로고치면, 당신은 많은 임의의 배들을 볼 수 있을 것입니다.

움직임을 위해, 간단함을 유지하고 컨테이너가 1초에 한 번 움직이도록 합시다. 이를 위해, 우리는 melonjs 타이머를 사용할 수 있습니다.

enemy_manager.js에 이 두 메소드를 추가합시다.

onActivateEvent : function () {
    var _this = this;
    this.timer = me.timer.setInterval(function () {
        _this.pos.x += _this.vel;
    }, 1000);
},

onDeactivateEvent : function () {
    me.timer.clearInterval(this.timer);
}

그리고 다음으로 init 메소드에서 속성을 16으로 설정합시다:

this.vel = 16;

오브젝트가 게임 세계에 추가될 때 onActivateEvent는 호출됩니다(만약 그것이 정의되었다면). 이는 당신이 컨테이너에서 addChild로 전달하는 어떤 오브젝트로든 가게 됩니다. 마찬가지로, onDeactivateEvent는 오브젝트가 게임 세계에서 제거되었을 때 호출됩니다.

setInterval의 MelonJS 버전을 사용해 (게임 루프에 내장되어, window.setInterval를 사용하지 않습니다), 우리는 이제 x 위치를 더할 수 있습니다.

저장하고 브라우저를 새로고침합시다. 적의 배들이 이제 모두 함께 움직입니다

그러면 removeChildNow 대상을 추가합시다:

removeChildNow : function (child) {
    this._super(me.Container, "removeChildNow", [child]);
    this.updateChildBounds();
}

removeChild 대신 removeChildNow를 확장하는 이유는, removeChild는 현재 프레임이 완료된 후 호출되기 때문입니다. removeChildNow는 실제로 오브젝트를 제거하는 메소드이고, 우리는 오브젝트가 제거된 뒤 바운드의 크기를 재설정하기를 원합니다.

onActivateEvent : function () {
    var _this = this;
    this.timer = me.timer.setInterval(function () {
        var bounds = _this.childBounds;

        if ((_this.vel > 0 && (bounds.right + _this.vel) >= me.game.viewport.width) ||
            (_this.vel < 0 && (bounds.left + _this.vel) <= 0)) {
            _this.vel *= -1;
            _this.pos.y += 16;
            if (_this.vel > 0) {
              _this.vel += 5;
            }
            else {
              _this.vel -= 5;
            }
        }
        else {
            _this.pos.x += _this.vel;
        }
    }, 1000);
}

상당한 양의 코드이므로 한 번 쪼개봅시다.

종속 바운드를 사용하여, 우리는 세계를 조정하기 위한 왼쪽 & 오른쪽 값을 얻어낼 수 있습니다.

var bounds = _this.childBounds;

첫 번째 파트는 만약 컨테이너가 오른쪽으로 움직이는지 체크하고, 오른쪽 모서리와 속도의 합이 뷰포트 바깥에 있게 합니다.

(_this.vel > 0 && (bounds.right + _this.vel) >= me.game.viewport.width)

두 번째 파트는 컨테이너가 왼쪽으로 움직이는지 체크하고, 그것의 왼쪽 바운드는 0보다 작아집니다.

(_this.vel < 0 && (bounds.left + _this.vel) <= 0)

블록에서, 우리는 속도를 뒤집고 16픽셀만큼 아래로 움직인 뒤 속도를 증가시킵니다.

_this.vel *= -1;
_this.pos.y += 16;
if (_this.vel > 0) {
    _this.vel += 5;
}
else {
    _this.vel -= 5;
}

그리고 마지막 비트로, 컨테이너가 왼쪽이나 오른쪽으로 움직이지 않았다면 우리는 속도를 증가시킵니다.

else {
    _this.pos.x += _this.vel;
}

이번에는 저장하고 새로고침하세요, 그것인 이제 스크린을 넘어 앞뒤로 움직이고, 우리 플레이어에 가까워질 수 있을 것입니다. 그러나 문제가 있습니다! 그것이 끝으로 가면 돌지 않습니다! 무슨 일이 벌어지고 있는 거죠?

이 시점에서, 당신은 URL에 #debug를 추가해야 합니다: http://localhost:8000/#debug 그리고 디버그 패널의 "히트박스" 옆의 체크박스를 탭하세요. 이것은 히트박스 렌더링을 활성화할 것이고, 당신은 당신의 오브젝트들의 내부 구조를 시각화할 수 있을 것입니다.

히트 박스는 종속 바운드(EnemyManager의 원래 위치를 차지하는 큰 보라색 직사각형)가 컨테이너와 움직이고 있지 않다는 것을 보여줄 것입니다. 이는 종속 바운드 업데이트가 자동적으로 계산되지 않기 때문입니다. 당신은 업데이트 메소드에 과부하가 걸리게 함으로써 쉽게 종속 바운드를 다시 계산할 수 있습니다:

update : function (time) {
    this._super(me.Container, "update", [time]);
    this.updateChildBounds();
}

저장하고 새로고치시면, 당신은 기대된 대로 작동하는 것을 발견할 수 있을 것입니다!

레이어 추가, 퓨 퓨!

이 게임 안에 몇몇 실제 "게임”을 넣을 시간입니다.

먼저 해야할 일은 당신의 play.js 파일을 열고, keybind & unbind를 새로 추가하는 것입니다:

me.input.bindKey(me.input.KEY.SPACE, "shoot", true);
me.input.unbindKey(me.input.KEY.SPACE);

bindKey 호출 안의 불리안의 이유는 키 누르기마다 등록될 수 있도록 하기 위해서입니다. 그래서 두 번 발사하려면, the player must press the space bar, release it, and then press it again.

우리가 플레이어가 발사할 수 있게 연결하기 전, 우리는 레이저가 필요합니다. laser.js 파일을 만드시고, 다음의 코드를 추가하세요. 언제나처럼, laser.js 스크립트 태그가 index.html 파일에 추가되었는지 확인하세요.

game.Laser = me.Entity.extend({
    init : function (x, y) {
        this._super(me.Entity, "init", [x, y, { width: game.Laser.width, height: game.Laser.height }]);
        this.z = 5;
        this.body.setVelocity(0, 300);
        this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;
        this.renderable = new (me.Renderable.extend({
            init : function () {
                this._super(me.Renderable, "init", [0, 0, game.Laser.width, game.Laser.height]);
            },
            destroy : function () {},
            draw : function (renderer) {
                var color = renderer.getColor();
                renderer.setColor('#5EFF7E');
                renderer.fillRect(0, 0, this.width, this.height);
                renderer.setColor(color);
            }
        }));
        this.alwaysUpdate = true;
    },

    update : function (time) {
        this.body.vel.y -= this.body.accel.y * time / 1000;
        if (this.pos.y + this.height <= 0) {
            me.game.world.removeChild(this);
        }

        this.body.update();
        me.collision.check(this);

        return true;
    }
});

game.Laser.width = 5;
game.Laser.height = 28;

그럼 이제 넘어가 봅시다. 바닥에서, 우리는 레이저의 너비 & 높이 속성을 설정했고, 이제 그것은 쉽게 재사용될 수 있을 것입니다.

game.Laser.width = 5;
game.Laser.height = 28;

전통적인 것이 여기 있습니다. x & y 의 위치를 파라미터와 너비+높이 속성으로부터 설정합니다. 우리의 다른 오브젝트와 약간 다르게, 우리는 오브젝트의 z 인덱스를 수동으로 설정했습니다. 이것은 addChild 호출의 z 인덱스를 넘기는 대안입니다.

this._super(me.Entity, "init", [x, y, { width: game.Laser.width, height: game.Laser.height }]);
this.z = 5;

다음에 설정할 메소드는 물리학 바디입니다. 우리가 이것을 사용해 스크린에 걸친 레이저를 이동시킬 것입니다.

this.body.setVelocity(0, 300);
this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;

기본 값으로, me.Body는 당신을 위해 모양을 설정하지 않을 것입니다. me.Entity는 위치, 너비, 높이를 기반으로 모양을 생성할 것입니다.

먼저 우리는 속도를 설정합니다. 속도는 벡터이고, 우리는 레이저가 올라가기를 원합니다. 그러므로 속도를 300으로 설정합니다. 속도는 방향을 표현하기 위해 절대 음수가 될 수 없다는 것을 기억하세요.

this.body.setVelocity(0, 300);

먼저 우리는 속도를 설정합니다. 속도는 벡터이고, 우리는 레이저가 올라가기를 원합니다. 그러므로 속도를 300으로 설정합니다. 속도는 방향을 표현하기 위해 절대 음수가 될 수 없다는 것을 기억하세요.

this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;

이것은 아주 알찬 코드 비트입니다. me.Renderable은 melon의 기본 그리기 클래스입니다. 그것은 게임 세계의 유효한 그리기 오브젝트에 필요한 최소한의 설정을 제공합니다.

this.renderable = new (me.Renderable.extend({
    init : function () {
        this._super(me.Renderable, "init", [0, 0, game.Laser.width, game.Laser.height]);
    },
    destroy : function () {},
    draw : function (renderer) {
        var color = renderer.getColor();
        renderer.setColor('#5EFF7E');
        renderer.fillRect(0, 0, this.width, this.height);
        renderer.setColor(color);
    }
}));

init은 매우 전통적이지만, 우리는 x & y를 0으로 설정해 이것이 레이저 엔티티에 상대적으로 되도록 할 것입니다. 엔티티 자체의 너비 & 높이도 같게 설정해 주세요.

init: function () {
    this._super(me.Renderable, "init", [0, 0, game.Laser.width, game.Laser.height]);
},

빈 파괴 메소드는 렌더러블이 제거되었을 때를 위한 콜백입니다. 우리는 콜백 로직을 정의할 필요가 없지만, 함수는 여전히 정의되어야 합니다.

destroy : function () {},

다음으로 우리가 그리기 메소드를 실행하려합니다. 여태까지 우리가 사용한 클래스는 우리의 목적에 맞는 그리기 메소드를 제공했고, 이제 우리는 렌더러를 사용해 손으로 무언가를 그릴 것입니다.

draw : function (renderer) {
    var color = renderer.getColor();
    renderer.setColor('#5EFF7E');
    renderer.fillRect(0, 0, this.width, this.height);
    renderer.setColor(color);
}

렌더러는 me.video.init의 설정에 따라 me.CanvasRenderer 또는 me.WebGLRenderer이 될 것입니다. 렌더러는 기본 그리기 옵션을 제공합니다.

그래서 우리는 먼저 원래 색의 레퍼런스를 얻어야 합니다. getColor()의 결과 값은 me.Color의 인스턴스입니다.

var color = renderer.getColor();

색을 더 멋진 초록색 레이저로 설정합시다.

renderer.setColor('#5EFF7E');

직사각형 채우기 숫자(fill rect number)를 사용합시다. 다시 말하지만, 0, 0은 상대적입니다. 그러므로 우리가 채우려는 직사각형의 너비 & 높이를 사용해 우리가 채우려는 직사각형의 크기를 정해줍시다

renderer.fillRect(0, 0, this.width, this.height);

이 색을 되돌립시다. 이것은 중요하기에 우리의 다른 그리고 호출이 색 변화에 의해 영향받지 않을 것입니다.

renderer.setColor(color);

일반적으로 당신은 당신의 게임을 이 순수한 캔버스 그리기 호출을 사용해 생성해야 합니다만, 캔버스를 언제 어떻게 쓰는지 아는 것이 훨씬 유용합니다.

레이저를 위한 마지막 단계는 init 메소드입니다:

init : function (x, y) {
    // ...
    this.alwaysUpdate = true;
}

alwaysUpdate 속성은 가능한 한 피하는 것이 좋습니다. 그것이 뷰포트 바깥에 있을 때 업데이트를 업데이트할 것입니다. 그것을 이 게임 안에서 사용하는 이유는 우리가 레이저를 그것이 화면 밖으로 사라질 때까지 사라지지 원치 않기 때문입니다. 우리가 그것이 화면 밖으로 나갈 때까지 기다리고, alwaysUpdate가 false라면, 절대 제거되지 않을 것입니다.

업데이트 메소드 부분입니다.

update : function (time) {
    this.body.vel.y -= this.body.accel.y * time / 1000;
    if (this.pos.y + this.height <= 0) {
        me.game.world.removeChild(this);
    }

    this.body.update();
    me.collision.check(this);

    return true;
}

첫번째 비트는 어떻게 우리가 레이저를 움직일 지입니다. 배는 직접 위치를 조정함으로써 움직입니다. 이것이 충돌 바디이므로, y 가속을 뺌으로써 우리는 y 속도를 조정할 것입니다.

this.body.vel.y -= this.body.accel.y * time / 1000;

만약 레이저의 위치와 높이(레이저의 바닥)이 0보다 작다면, 우리는 게임 세상에서 레이저를 제거할 수 있습니다. Again, this will function now work because alwaysUpdate is set to true.

if (this.pos.y + this.height <= 0) {
    me.game.world.removeChild(this);
}

바디 업데이트는 매우 중요한데, 이것은 우리가 이 프레임에 설정한 속도를 적용합니다.

this.body.update();

그리고 우리는 tell MelonJS에게 me.collision.check(this)를 가지고 이 오브젝트와의 충돌을 확인하라고 말합니다.

me.collision.check(this);

다음 단계는 이 기능을 위한 것으로, 레이저를 엔티티 풀에 추가합니다. Add the following code to game.js, same as the Player & Enemy objects.

me.pool.register("laser", game.Laser);

그러면 player.js 파일로 돌아가, 업데이트 메소드 안에 레이저 발사를 추가해봅시다:

if (me.input.isKeyPressed("shoot")) {
    me.game.world.addChild(me.pool.pull("laser", this.pos.x - game.Laser.width, this.pos.y - game.Laser.height))
}

게임을 장전하고 발사를 시도하세요. 당신은 레이저가 발사되는 것을 볼 수 있습니다. 그러나 그들은 아무 것과도 충돌하지 않았습니다.

충돌

먼저, 우리의 적에게 물리적 바디를 줍시다. 이것을 enemy.js의 init 메소드에 붙입니다

this.body.setVelocity(0, 0);
this.body.collisionType = me.collision.types.ENEMY_OBJECT;

그리고 우리가 바디를 업데이트할 수 있도록 업데이트 메소드를 추가합니다:

update : function (time) {
    this._super(me.Entity, "update", [time]);

    this.body.update();

    return true;
}

이제 laser.js file로 충돌 핸들러를 추가해봅시다.

onCollision : function (res, other) {
    if (other.body.collisionType === me.collision.types.ENEMY_OBJECT) {
        me.game.world.removeChild(this);
        game.playScreen.enemyManager.removeChild(other);
        return false;
    }
}

우리가 사용하지 않고 있는 res 파라미터는, 단순히 충돌 결과입니다. 그러므로 그것은 얼마나 많은 오버랩이 있었는지, 어디에서 충돌이 있었는지 등 세부 사항을 포함합니다.

우리가 충돌 유형을 적의 몸에 ENEMY_OBJECT로 설정했기 때문에, 우리는 그 타입의 오브젝트와 레이저가 충돌한 것을 확인할 수 있습니다.

if (other.body.collisionType === me.collision.types.ENEMY_OBJECT) {

그러면 우리는 적 매니저 컨테이너로부터 적을 없앨 때, 레이저로부터 적을 제거합니다.

me.game.world.removeChild(this);
game.playScreen.enemyManager.removeChild(other);

false가 출력되는 이런 경우는 엄밀하게 말해 필요하지 않습니다만, 지적하는 것은 중요합니다. 만약 MelonJS의 충돌 핸들러로부터 false 값이 출력된다면, 오브젝트는 통과할 것입니다. 만약 true가 출력된다면, 하드 스탑이 될 것입니다.

변경 사항을 저장하고, 브라우저를 다시 로드해봅시다. 이제 당신은 적의 배를 볼 수 있을 것입니다.

다음 단계는, 승리 & 패배 조건을 추가하는 것입니다.

승리 & 패배 조건

이 게임의 마지막 단계는 사실 승리 & 패배 조건을 추가하는 것입니다. 조건 그자체는 굉장히 명확합니다. 배가 플레이어 근처로 오면, 플레이어는 집니다. 플레이어가 모든 적의 배를 파괴한다면, 그들이 이깁니다.

그럼 게임이 끝나면 무슨 일이 일어날까요? 많은 경우 당신은 플레이어가 이겼는지 졌는지와 같은 종류의 화면을 보여주려고 할 것입니다. 이것을 간단하게 유지하고 당신에게 또다른 조그만 트릭을 보여드리자면, 우리는 그냥 게임을 리셋하면 됩니다. 그래서 그것이 다시 시작하는 것이죠.

먼저, 우리는 패배 조건을 할 것입니다

이것에 대한 유사코드는 다음과 같을 것입니다:

if enemy manager overlaps player
  then end game
else
  continue
end

플레이화면(PlayScreen)은 우리의 현재 게임 상태입니다. 그것은 플레이어에게 레퍼런스를 가지고, 그것은 상태를 리셋할 능력을 가지고 있습니다. 그러므로 저기에 패배 조건을 확인하는 로직을 추가해 봅시다.

checkIfLoss : function (y) {
    if (y >= this.player.pos.y) {
       this.reset();
    }
},

onResetEvent 메소드 위에 그것을 추가합니다. 그것은 Y 값을 받고, 만약 플레이어를 넘어서는지 확인합니다. 그리고 그것의 리셋 메소드를 호출합니다. 리셋은 게임 세계로부터 모든 오브젝트를 날려버릴 것이고, 상태를 다시 로드할 것입니다. 그러므로 그것은 onResetEvent를 다시 호출하고, 적들과 플레이어를 다시 살게 합니다.

이제 이 컨디션 확인을 호출하기 위해, 단순히 적 관리자의 우리의 간격에 메소드를 추가합시다:

this.timer = me.timer.setInterval(function () {
    var bounds = _this.childBounds;

    if ((_this.vel > 0 && (bounds.right + _this.vel) >= me.game.viewport.width) ||
        (_this.vel < 0 && (bounds.left + _this.vel) <= 0)) {
        _this.vel *= -1;
        _this.pos.y += 16;
        if (_this.vel > 0) {
          _this.vel += 5;
        }
        else {
          _this.vel -= 5;
        }
        game.playScreen.checkIfLoss(bounds.bottom); // <<<
    }
    else {
       _this.pos.x += _this.vel;
    }
}, 1000);

우리가 checkIfLoss 메소드를 확인하고 있었기에 만약 전달된 숫자가 플레이어의 Y값보다 크다면, 우리는 컨테이너의 바닥 끝으로 그것을 보낼 필요가 있습니다, 그것은 bounds.bottom으로 튕겨나갈 것입니다.

이제 문제는 만약 당신이 그것을 실행한다면, this.player이 정의되지 않는다는 것입니다. 왜냐하면 우리가 플레이 화면에서 그 속성을 설정해주지 않았기 때문입니다. 당신은 그 속성을 다음과 같이 설정할 수 있습니다:

onResetEvent의 라인 대체:

me.game.world.addChild(me.pool.pull("player"), 1);

이와 함께:

this.player = me.pool.pull("player");
me.game.world.addChild(this.player, 1);

저장하시고 브라우저를 새로고침하세요. this.player는 이제 잘 작동할 것이고, 우리의 새로운 메소드 호출도 작동할 것입니다. 적들이 일 분 간 움직이게 한 뒤, 게임 리셋을 지켜보세요.

승리 조건

마찬가지로, 우리는 일단 플레이어가 이기면 게임을 리셋할 것입니다. 일단 모든 배가 사라지면 승리를 야기하고자 하기에, 적 관리자 상 종속된 것들의 길이를 확인할 수 있습니다.

먼저 이 불리언을 createEnemies 메소드 바닥에 추가하세요:

this.createdEnemies = true;

적 관리자에 다음 업데이트 메소드를 추가하세요:

update : function (time) {
    if (this.children.length === 0 && this.createdEnemies) {
        game.playScreen.reset();
    }
    this._super(me.Container, "update", [time]);
    this.updateChildBounds();
}

이것은 매우 간단합니다. 종속은 배열이므로, 우리는 그 길이가 0이 되는지 확인하고, 그것이 경제와 우선으로 만들었는지를 확인합니다. 불리안 체크 없이, 그것이 어떤 종속도 가지고 있지 않기에 게임이 스스로를 계속 리셋하려고 할 수 있습니다.

저장하고 브라우저를 새로고침 하세요. 모든 배들을 동시에 꺼내려고 시도하시고 게임 리셋을 보세요.

도전

저희는 이 몇몇을 이 튜토리얼 바깥에 남겨둠으로써, 당신이 스스로 탐구할 수 있도록 할 것입니다. 이것은 프로그래밍과 게임 개발의 중요한 부분입니다.

만약 당신이 튜토리얼의 도전들이나 부분들 중 어떤 부분에서 막히신다면, 문제를 검색하시거나, 포럼@html5gamedevs에서 저희에게 질문해주세요

도전 #1

적절한 승리 & 패배 화면 추가

  1. 이 화면들은 추가적 ScreenObjects 를 게임에 추가함으로써 만들어집니다, 이들을 game.js에 등록하고, 상태를 바꿉니다. 승리 & 패배 화면에 어떤 상태를 쓸 지를 위해서는, 이용 가능한 상태들을 보십시오: http://melonjs.github.io/melonJS/docs/me.state.html
  2. 승리와 패배 화면은 스프라이트, 텍스트, 또는 둘 다를 포함할 수 있습니다. 당신이 정말로 무엇을 원하든 간에요. me.Font와 me.Sprite를 보세요. me.Font 오브젝트를 표시하려면, me.Font의 인스턴스를 포함하는 me.Renderable의 인스턴스를 사용하시고, me.Font#draw를 적용하기 위해 draw 함수를 실행하세요.
  3. 당신에게 새로운 패배 스크린을 대신 보여주기 위해 checkIfLoss 메소드를 조정하세요.
  4. 당신의 승리 screneobject로 상태를 바꾸기 위해 EnemyManager의 업데이트 메소드 안의 if 블록을 조정하세요.
  5. 더 많은 보너스에도 불구하고, 플레이어에게 플레이 방법을 말해주는 메뉴 화면을 추가합니다.

도전 #2

UI 추가

  1. 적 카운터를 추가하면, 적 속도가 오른쪽 위/왼족 구석으로 옮겨질 것입니다. 이들 속성은 다음을 통해 얻어질 수 있습니다: game.playScreen.enemyManager.children.length game.playScreen.enemyManager.vel
  2. me.Font를 다시 보고, 텍스트를 그리기 위해 렌더러블을 실행합니다. UI 조각들 모두에서 사용될 수 있는 렌더러블을 확장한 하나의 클래스 만을 사용하려고 해 봅시다.
  3. 점수 요소를 추가합시다. 플레이 화면에서 점수를 추적합니다. 적이 죽었을 때마다 그것을 업데이트합니다. 적들이 충돌 핸들러로부터 레이저로 제거된 것을 기억하세요.

도전 #3

레벨의 개념 추가

  1. 당신이 라운드를 깬 이후, 같은 라운드를 다시 실행하는 대신, 더 빠르게 시작하는 새로운 라운드를 합시다. 여기서의 주요 로직은 game.js에서 라운드 카운트를 계속하는 것이고, 매번 승리 때마다 그것을 증가시킵니다. 그리고 카운트를 적 매니저에서 속도를 구성하기 위해 사용하는 것뿐입니다.
  2. 각 라운드 프로세스가 너무 빠르도록 해 봅시다(예를 들어 각 Y 항목에 대해 +5만큼 더해 + 8 ). 좋은 느낌을 받을 때까지 약간의 숫자와 함께 플레이해 봅시다.