Tutorial/2DPlatformer の変更点


[[Tutorial]]

* 概要 [#ka33becb]
このページは、HaxeFlixelの機能を使って2Dプラットフォーマーを作成するチュートリアルとなります

----
#contents
----

** テストプロジェクトの作成 [#m5ef65cc]
任意のフォルダで以下のコマンドを実行し、プロジェクトを作成します

 flixel tpl -n "Platformer"

Main.hxを開いて、画面サイズを160x120に小さくします

#geshi(ActionScript){{
class Main extends Sprite 
{
  var gameWidth:Int = 160; // Width of the game in pixels (might be less / more in actual pixels depending on your zoom).
  var gameHeight:Int = 120; // Height of the game in pixels (might be less / more in actual pixels depending on your zoom).
  var initialState:Class<FlxState> = MenuState; // The FlxState the game starts with.
  ……
}}
** マップの表示 [#b8f40c65]
+マップデータをCSV(カンマ区切りのテキスト)で用意する
+CSVをFlxTilemapで読み込んで表示する
*** 1. マップデータをCSV(カンマ区切りのテキスト)で用意する [#q83fb0bd]
まずはマップデータをCSVで用意します。
マップエディタ(例えばTiledMapEditorなど)を使って用意してもよいです。

#geshi(csv){{
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,
1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,1,
1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,
1,1,1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,1,1,1,0,0,1,1,1,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
}}

マップサイズは、20x15としました。

*** 2. CSVをFlxTilemapで読み込んで表示する[#o879e83b]
MenuState.hx を開いて、create関数に以下のコードを追加します。

#geshi(ActionScript){{
  override public function create():Void
  {
    super.create();
    var csv = "1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,
1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,1,
1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,
1,1,1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,1,1,1,0,0,1,1,1,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1";
    var map = new FlxTilemap();
    map.loadMap(csv, GraphicAuto, 0, 0, FlxTilemap.AUTO);
    add(map);
  }
}}

実行するとマップが表示されます。
#ref(001.png);


※HaxeFlixel Ver.4からAPIが変わったため、Ver.4以降を使用する場合は以下のコードとなります。

#geshi(ActionScript){{
package;

import flixel.FlxG;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.text.FlxText;
import flixel.ui.FlxButton;
import flixel.math.FlxMath;
import flixel.tile.FlxTilemap;
import flixel.tile.FlxBaseTilemap;
import flixel.system.FlxAssets;
import flixel.graphics.FlxGraphic;
import flixel.tile.FlxTilemap;

class MenuState extends FlxState
{
  override public function create():Void
  {
    super.create();
    var csv = "1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,
1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,1,
1,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,
1,1,1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,1,1,1,0,0,1,1,1,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1";
    var map = new FlxTilemap();
    map.loadMapFromCSV(csv, FlxGraphic.fromClass(GraphicAuto), 0, 0, FlxTilemapAutoTiling.AUTO);
    add(map);
  }

  override public function update(elapsed:Float):Void
  {
    super.update(elapsed);
  }
}

}}
*** 3. CSVを外部データにする [#i6d69214]
CSVをプログラムに埋め込む方法からテキストファイルにして外部から読み込む方法に修正します。
先ほどのカンマ区切りのデータを「leve.csv」として「assets/data/level.csv」に保存します。
そして、読み込み部分を以下のように修正します。

#geshi(ActionScript){{
  override public function create():Void
  {
    super.create();
    var csv = openfl.Assets.getText("assets/data/level.csv");
    var map = new FlxTilemap();
    map.loadMap(csv, GraphicAuto, 0, 0, FlxTilemap.AUTO);
    add(map);
  }
}}
*** 解説 [#s185400a]
- CSVのマップデータは「0」が何もないところで、「1」が壁となります
- FlxTilemap.loadMapの引数の説明は以下のとおりです
-- 1: マップデータ
-- 2: タイル画像
-- 3: タイルの幅
-- 4: タイルの高さ
-- 5: タイリングモード
- GraphicAutoは、HaxeFlixelに組み込み済みのタイル画像です。画像データは「{Haxeのインストールフォルダ}/haxe/lib/flixel/#,#,#/assets/images/tile」フォルダに存在します
#ref(autotile.png);
- 1つのタイルのサイズは 8x8 です
- タイリングモードの「FlxTilemap.AUTO」は、壁のつなぎを自動で計算するオートタイルモードとなります

*** プロジェクトデータ [#db4f063f]
ここまでの内容を実装したプロジェクトデータです (FlashDevelopのプロジェクトファイル付き)

#ref(Platformer.zip);
** プレイヤーの実装 [#rccb3381]

+ プレイヤーの作成
+ マップとプレイヤーとの衝突判定

*** 1. プレイヤーの作成 [#yf4fbf64]
Player.hx を追加し、以下のように記述します。

#geshi(ActionScript){{
import flixel.FlxSprite;
import flixel.FlxG;
import flixel.util.FlxColor;
import flixel.FlxObject;

class Player extends FlxSprite {
  public function new (X:Float, Y:Float) {
    super(X, Y);
    makeGraphic(8, 8, FlxColor.RED);
    acceleration.y = 200; // 重力
    maxVelocity.set(50, 200); // 速度制限
  }

  override public function update():Void {
    // 動かない場合は停止
    velocity.x = 0;
    if(FlxG.keys.pressed.LEFT) {
      // 左に動く
      velocity.x = -50;
    }
    else if(FlxG.keys.pressed.RIGHT) {
      // 右に動く
      velocity.x = 50;
    }
    if(isTouching(FlxObject.FLOOR)) {
      // 床に着地している
      if(FlxG.keys.justPressed.SPACE) {
        // ジャンプする
        velocity.y = -100;
      }
    }

    // velocityを設定した後で更新
    super.update();
  }
}
}}

ポイントは、super.update の呼び出しタイミングです。 velocity を設定した後で呼び出さないとジャンプができません。また、ジャンプできるかどうかは「isTouching(FlxObject.FLOOR)」で判定をします。これは床に着地しているかどうかの判定です。この判定を行わないと、無限に空中ジャンプができてしまいます。

*** 2. マップとプレイヤーとの衝突判定 [#dfc003a5]
次に、MenuState.hx を修正します。

#geshi(ActionScript){{
package;

import flixel.FlxG;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.text.FlxText;
import flixel.ui.FlxButton;
import flixel.util.FlxMath;
import flixel.tile.FlxTilemap;

class MenuState extends FlxState
{
  var _map:FlxTilemap; // マップ
  var _player:Player;  // プレイヤー

  override public function create():Void
  {
    super.create();

    // マップ作成
    var csv = openfl.Assets.getText("assets/data/level.csv");
    _map = new FlxTilemap();
    _map.loadMap(csv, GraphicAuto, 0, 0, FlxTilemap.AUTO);
    add(_map);

    // プレイヤー作成
    _player = new Player(32, 32);
    add(_player);
  }

  override public function destroy():Void
  {
    super.destroy();
  }

  override public function update():Void
  {
    super.update();

    // マップとプレイヤーの衝突判定
    FlxG.collide(_map, _player);
  }
}
}}

衝突判定は、FlxG.collide を呼び出すだけです。
実行すると、左右キーで移動、スペースキーでジャンプできます。

#ref(002.png);
** マップのスクロールの実装 [#o1084e98]
+ マップを広くする
+ カメラにスクロールを設定する
+ コリジョンの有効範囲を広くする

*** 1. マップを広くする [#c390bd78]
まずはスクロールを有効にするためマップを広くします。現在は 160x120 で画面サイズと同じですので、これを 320x240 にします。

#ref(level.csv);

データが大きいので添付ファイルにしました。これを assets/data/level.csv に上書きます。

*** 2. カメラにスクロールを設定する [#jd66f461]
MenuState.hx を以下のように修正します。

#geshi(ActionScript){{
  override public function create():Void
  {
    super.create();

    // マップ作成
    var csv = openfl.Assets.getText("assets/data/level.csv");
    _map = new FlxTilemap();
    _map.loadMap(csv, GraphicAuto, 0, 0, FlxTilemap.AUTO);
    add(_map);

    // プレイヤー作成
    _player = new Player(32, 32);
    add(_player);

    // カメラを設定 (※ここを追加)
    FlxG.camera.follow(_player, FlxCamera.STYLE_PLATFORMER);
  }
}}

create関数で、FlxG.camera.follow(_player, FlxCamera.STYLE_PLATFORMER) を呼び出すだけとなります。この関数は第一引数にカメラが追いかけるオブジェクト、第二引数に追いかけるタイプ、を指定します。

では、実行して動作を確認します。スクロールするようになったのですが、右下へ進むとプレイヤーが奈落の底へ落下していきます。

#ref(003.png);

これは、HaxeFlixelはデフォルトでは、コリジョンが画面サイズ内でのみ有効になっているためです。

*** 3. コリジョンの有効範囲を広くする [#sa3499e4]
コリジョンを広げるには、FlxG.worldBounds を使用します。

#geshi(ActionScript){{
  override public function create():Void
  {
    super.create();
    
    // マップ作成
    var csv = openfl.Assets.getText("assets/data/level.csv");
    _map = new FlxTilemap();
    _map.loadMap(csv, GraphicAuto, 0, 0, FlxTilemap.AUTO);
    add(_map);
    
    // プレイヤー作成
    _player = new Player(32, 32);
    add(_player);
    
    // カメラを設定
    FlxG.camera.follow(_player, FlxCamera.STYLE_PLATFORMER);
    // コリジョンの有効範囲を広げる (※ここを追加)
    FlxG.worldBounds.set(0, 0, _map.width, _map.height);
  }
}}

FlxTilemap.width がマップ全体の幅、FlxTilemap.height がマップ全体の高さになるので、それを使ってコリジョンの有効範囲を広げます。

実行すると、右下へ進んでもコリジョンが正しく判定されるようになります。
** 一方向プラットフォーム(下から登れる床)の実装 [#h0270781]
+ Floorクラスの作成
+ Floorクラスの生成と当たり判定
+ 床からの「飛び降り」を実装する

***1. Floorクラスの作成 [#o8a11a09]
一方向プラットフォームを作成します。ファイル名は「Floor.hx」とします

#geshi(ActionScript){{
package;

import flixel.FlxSprite;
import flixel.FlxObject;

/**
 * 下から登れる床
 */
class Floor extends FlxSprite {
  public function new(X:Float, Y:Float):Void {
    super(X, Y);
    loadGraphic("assets/images/floor.png");

    // 上からのみ衝突を有効にする
    allowCollisions = FlxObject.UP;
    // 接触しても動かないようにする
    immovable = true;
  }
}
}}

ポイントは2つです。下から登れるようにするので、allowCollisions に FlxObject.UP を指定し、上からの当たり判定のみ有効にします。ただこれだけだと上に乗ったときに、床がプレイヤーの重みで落下してしまうので、immovable に true を設定して落下しないようにします。

なお一方向プラットフォームの画像はこちらのものを使用します。

#ref(floor.png);

8x8サイズのため、かなり小さくて分かりづらいですが、小さい茶色い横棒のような画像です。
右クリックでダウンロードしたら、「assets/images/」に配置します。

***2. Floorクラスの作成と当たり判定 [#k91a1c78]

次にFloorクラスの作成をします。MenuStateのフィールドに床を管理するFlxGroupの変数(_floors)を追加し、create関数を修正します。

#geshi(ActionScript){{

  var _map:FlxTilemap; // マップ
  var _floors:FlxGroup; // 床 (※ここを追加)
  var _player:Player;  // プレイヤー

  override public function create():Void
  {
    super.create();

    // マップ作成
    var csv = openfl.Assets.getText("assets/data/level.csv");
    _map = new FlxTilemap();
    _map.loadMap(csv, GraphicAuto, 0, 0, FlxTilemap.AUTO);
    add(_map);

    // プレイヤー作成
    _player = new Player(32, 32);
    add(_player);

    // 移動床配置 (※ここを追加)
    _floors = new FlxGroup();
    _floors.add(new Floor(80, 32));
    add(_floors);

    // カメラを設定
    FlxG.camera.follow(_player, FlxCamera.STYLE_PLATFORMER);
    // コリジョンの有効範囲を広げる
    FlxG.worldBounds.set(0, 0, _map.width, _map.height);
  }
}}

続けて、update関数を修正します。

#geshi(ActionScript){{
  override public function update():Void
  {
    super.update();

    // マップとプレイヤーの衝突判定
    FlxG.collide(_map, _player);
    // 床とプレイヤーの衝突判定 (※ここに追加)
    FlxG.collide(_floors, _player);
  }
}}

実行すると、下から登れる床が配置されます。

#ref(004.png);

***3. 床からの「飛び降り」を実装する [#e26875d3]
「飛び降り」を実装する方針は以下のとおりです。
+プレイヤーに飛び降り有効タイマーを持たせる
+プレイヤーが飛び降り中であれば「床」との衝突判定を行わない

まずはPlayer.hxを修正します。
#geshi(ActionScript){{
class Player extends FlxSprite {

  var _tDropDown:Int = 0; // 飛び降りタイマー (※ここを追加)

  // 飛び降り中かどうか (この関数を追加)
  public function isDropDown():Bool {
    return _tDropDown > 0;
  }

  public function new (X:Float, Y:Float) {
    super(X, Y);
    makeGraphic(8, 8, FlxColor.RED);
    acceleration.y = 200; // 重力
    maxVelocity.set(50, 200); // 速度制限
  }

  override public function update():Void {
    // 動かない場合は停止
    velocity.x = 0;
    if(FlxG.keys.pressed.LEFT) {
      // 左に動く
      velocity.x = -50;
    }
    else if(FlxG.keys.pressed.RIGHT) {
      // 右に動く
      velocity.x = 50;
    }
    if(isTouching(FlxObject.FLOOR)) {
      // 床に着地している
      // 飛び降り判定 (※ここを追加)
      if(FlxG.keys.pressed.DOWN && FlxG.keys.justPressed.SPACE) {
        // 飛び降りる
        _tDropDown = 16;
      }
      // ※else を忘れないように!
      else if(FlxG.keys.justPressed.SPACE) {
        // ジャンプする
        velocity.y = -100;
      }
    }

    // velocityを設定した後で更新
    super.update();

    // 飛び降りタイマー更新 (※ここを追加)
    if(_tDropDown > 0) {
      _tDropDown--;
    }
  }
}
}}

- まず、フィールドに落下状態を管理する「_tDropDown」を追加します。この値が0より大きければ落下中と見なします
- 次に、「isDropDown」関数を追加します。これは落下中かどうかを判定する関数となります
- そして、「下キー」+「SPACEキー」で飛び降りを開始します。飛び降りが有効なフレーム数は「16」としましたが、落下速度(重力加速度)に応じてこの値は変化します
- 最後に、毎回のupdateで飛び降りタイマーが0より大きければ、1ずつ減らしていきます

では、MenuStateのupdate関数に判定を追加します。

#geshi(ActionScript){{
  override public function update():Void
  {
    super.update();

    // マップとプレイヤーの衝突判定
    FlxG.collide(_map, _player);
    // ここを修正(※)
    if(_player.isDropDown() == false) {
      // 床とプレイヤーの衝突判定
      FlxG.collide(_floors, _player);
    }
  }
}}

飛び降り中は「床」との衝突判定を行わないように修正しました。

実行すると、「下キー」+「SPACEキー」で床から飛び降りられるようになりました。

** 移動床の実装 [#y0ead576]
*** 1. 左右に移動する床 [#y7540836]
まずは左右に移動する床を作ります。一方向床(Floor.hx)を改造して移動床にします

#geshi(ActionScript){{
class Floor extends FlxSprite {
  
  // 移動タイマー (※ここを追加)
  var _tMove:Int = 0;

  public function new(X:Float, Y:Float) {
    super(X, Y);
    loadGraphic("assets/images/floor.png");
    
    // 上からのみ衝突を有効にする
    allowCollisions = FlxObject.UP;
    // 接触しても動かないようにする
    immovable = true;
    
    // 最初は左に動く (※ここを追加)
    velocity.x = 50;
  }
  
  // 更新 (※この関数を追加)
  override public function update():Void {
    super.update();
    
    _tMove++;
    if(_tMove > 60) {
      // 移動方向を反転
      velocity.x *= -1;
      // 移動タイマー初期化
      _tMove = 0;
    }
  }
}
}}

修正点は以下のとおりです

- フィールドに移動タイマー「_tMove」を追加します [#t1af94d4]
- create関数で初期速度を設定します [#w418fb07]
- update関数を追加し、一定フレーム経過で移動速度を反転します [#scce2df0]

移動方向の反転をタイマーで行っていますが、これはimmovable を true に設定している影響で、FlxG.collide や FlxG.overlap ではうまく当たり判定を行うことができないためです。この問題に対策するには、衝突検知のために別のコリジョン(センサー)を用意するとよいでしょう。

*** 2. 上下に移動する床 [#g40bbd8d]
上下に移動する床は、移動方向をY方向に変更するだけです。
Floor.hx を以下のように修正します。

#geshi(ActionScript){{
class Floor extends FlxSprite {
  
  // 移動タイマー
  var _tMove:Int = 0;

  public function new(X:Float, Y:Float) {
    super(X, Y);
    loadGraphic("assets/images/floor.png");
    
    // 上からのみ衝突を有効にする
    allowCollisions = FlxObject.UP;
    // 接触しても動かないようにする
    immovable = true;
    
    // 最初は下に動く (※ここを修正)
    velocity.y = 50;
  }
  
  override public function update():Void {
    super.update();
    
    _tMove++;
    if(_tMove > 60) {
      // 移動方向を反転 (※ここを修正)
      velocity.y *= -1;
      // 移動タイマー初期化
      _tMove = 0;
    }
  }
}
}}

床の配置座標があまり良くないので、MenuState.hx を修正して、初期位置を変更します。

#geshi(ActionScript){{
  // 移動床配置
  _floors = new FlxGroup();
  _floors.add(new Floor(16, 64)); // ※ここを修正
  add(_floors);
}}

実行すると、上下に移動する床に合わせて、プレイヤーも上下に移動するようになります。

#ref(005.png);

*** 3. 上下移動床の問題点と解決策 [#lbee9c3f]
今回は問題なく動作しましたが、プレイヤーの重力加速度よりも床の上下移動が速いと、プレイヤーが着地状態と落下状態を繰り返すこととなってしまいます。
この問題を回避するには、プレイヤーが現在着地している床の情報を保持し、床のY座標の変化量(Floor.y - Floor.last.y)をプレイヤーの座標に足し込むことで、プレイヤーの座標を補正する必要があります。

*** プロジェクトデータ [#ja41b46d]
ここまでの内容を実装したプロジェクトファイルです(FlashDevelopのプロジェクトファイル付き)

#ref(moving_platform.zip);
** スロープ(斜面)の実装 [#tf9174d1]
TODO:
** 参考 [#c9f0fdc7]
- [[公式Demo「EZPlatformer」>http://haxeflixel.com/demos/EZPlatformer/]]
- [[公式Demo「Revenge」>http://haxeflixel.com/demos/Revenge/]]
- [[公式Demo「FlxTilemapExt」>http://haxeflixel.com/demos/FlxTilemapExt/]]
- [[The guide to implementing 2D platformers>http://higherorderfun.com/blog/2012/05/20/the-guide-to-implementing-2d-platformers/]]