今回はインベーダーゲームもどきを作ってみます。
公式のデモはこちらです。
http://haxeflixel.com/demos/FlxInvaders/
ソースコードはこちらですね。
https://github.com/HaxeFlixel/flixel-demos/tree/master/Arcade%20Classics/FlxInvaders/source
今回は画像ファイルを使用するので、こちらからダウンロードします。
https://github.com/HaxeFlixel/flixel-demos/tree/master/Arcade%20Classics/FlxInvaders/assets
- alien.png(敵画像)
- ship.png(自機画像)
必要なのは上記2つの画像となります(白い画像なのでプレビューしても何もないように見えますが、プログラムで描画してみると絵が描かれているのを確認できます)
ゲームオブジェクトの種類
今回登場するゲームオブジェクトは以下の5つとなります。
- PlayerShip:自機
- PlayerBullet:自機の弾
- Alien:エイリアン(敵)
- AlienBullet:敵弾
- Shield:シールド
設定ファイル(Main.hx)の編集
前回と同じように、画面サイズは320x240とし、PlayStateを起動、スプラッシュのスキップskipSplash
を有効にします。
class Main extends Sprite
{
var gameWidth:Int = 320;
var gameHeight:Int = 240;
var initialState:Class<FlxState> = PlayState;
var zoom:Float = -1;
var framerate:Int = 60;
var skipSplash:Bool = true; // 起動時のスプラッシュを無効
var startFullscreen:Bool = false;
自機の作成(PlayerShip.hx)
自機の表示
PlayerShit.hxをソースフォルダsource
に追加します。
そして以下のように記述します。
package ;
import flixel.FlxSprite;
import flixel.FlxG;
/**
* 自機
**/
class PlayerShip extends FlxSprite {
public function new() {
super(FlxG.width / 2 - 6, FlxG.height - 12, "assets/images/ship.png");
}
}
FlxSpriteのコンストラクタは、(配置するX座標, 配置するY座標, 読み込む画像)となり
ます。この例では、中心の画面下に配置し、ship.pngを読み込んでいます。なお画像の読み込みは"assets"からの相対パスを指定します。このパスは、Project.xmlに記述されている、assetsタグのpath要素がルートパスとなります。
<!--------------------------------PATHS SETTINGS-------------------------------->
<set name="BUILD_DIR" value="export" />
<classpath name="source" />
<assets path="assets" /> <!-- アセットパスの指定 -->
続けてPlayState.hxに自機のインスタンス生成を記述します。
class PlayState extends FlxState
{
private var _player:PlayerShip; // 自機
override public function create():Void {
super.create();
// 自機を配置
_player = new PlayerShip();
add(_player);
}
自機の移動
PlayerShip.hxを編集して自機の移動を実装します。(update関数を実装)
override public function update():Void {
super.update();
// 速度を初期化
velocity.x = 0;
// キー入力判定
if(FlxG.keys.anyPressed(["LEFT"])) {
// 左キーで左移動
velocity.x -= 100;
}
else if(FlxG.keys.anyPressed(["RIGHT"])) {
// 右キーで右移動
velocity.x += 100;
}
// 画面外に出ないようにする
if(x > FlxG.width - width - 4) {
x = FlxG.width - width - 4;
}
if(x < 4) {
x = 4;
}
}
実行すると左右キーで移動できます。
自機の弾の実装
自機の弾はPlayState.hxに実装します。
class PlayState extends FlxState {
private var _player:PlayerShip; // 自機
public var playerBullets:FlxGroup; // 自機の弾グループ
override public function create():Void {
super.create();
// 自機を配置
_player = new PlayerShip();
add(_player);
// 自機の弾を確保
var numPlayerBullets:Int = 8; // 8発のみ
playerBullets = new FlxGroup(numPlayerBullets);
for( i in 0...numPlayerBullets) {
var sprite:FlxSprite = new FlxSprite(-100, -100);
sprite.makeGraphic(2, 8);
sprite.exists = false; // 確保するだけなので存在しないことにする
playerBullets.add(sprite);
}
// 自機の弾グループを登録
add(playerBullets);
}
ここでは弾は「8発」だけ確保しています。可変長ではなくサイズを固定化することで、断片化やガベコレによる速度低下を軽減できます。
PlayerShip.hxに戻って、弾を発射する処理を実装します。
override public function update():Void {
// ...(省略)
// SPACEキーで弾を発射する
if(FlxG.keys.justPressed.SPACE) {
// FlxGから実行中のStateを取得
var state:PlayState = cast(FlxG.state, PlayState);
// 未使用の弾を取得
var bullet:FlxSprite = cast(state.playerBullets.recycle(), FlxSprite);
// 発射位置を自機の真ん中にする
bullet.reset(x + width/2 - bullet.width/2, y);
// 上方向に移動させる
bullet.velocity.y = -140;
}
FlxGというゲーム管理オブジェクトから、PlayStateを取得しフィールドのplayerBulletのrecycle()を呼び出し、未使用のFlxSpriteを取り出します。その後、発射位置を調整して、上方向に移動しています。
実行するとSPACEキーで弾を撃てるようになります。
連打すると分かるのですが、インスタンスが足りない場合に recycle() は最初に生成したインスタンスを使いまわします。これにより消滅処理を呼ばなくても使いまわせるので処理を簡潔に書くことができます(ただしゲーム的にマズい場合は、画面外に出たら消滅する判定・生存数のチェックが必要となります)。
##エイリアンの作成(Alien.hx)
モジュールAlien.hxをプロジェクトに追加して、以下のように記述します。
package ;
import flixel.util.FlxRandom;
import flixel.group.FlxGroup;
import flixel.FlxSprite;
/**
* 敵クラス
**/
class Alien extends FlxSprite {
private var _shotClock:Float; // 弾を撃つ間隔
private var _originalX:Int; // 最初のX座標
private var _bullets:FlxGroup; // 敵弾
/**
* コンストラクタ
* @param x 初期座標(X)
* @param y 初期座標(&)
* @param color 色
* @param bullets 敵弾グループ
**/
public function new(x:Int, y:Int, color:Int, bullets:FlxGroup) {
// FlxSprite初期化
super(x, y);
// 画像ファイル読み込み (第2引数に true 指定でアニメーションファイルとなる)
loadGraphic("assets/images/alien.png", true);
// 色
this.color = color;
// 初期位置を保持
_originalX = x;
// ショットタイマーを初期化
resetShotClock();
// アニメーションを作成
this.animation.add("Default", [0, 1, 0, 2], Math.floor(6 + FlxRandom.float() * 4));
// アニメーションを再生
this.animation.play("Default");
// 移動量を設定
velocity.x = 10;
// 敵弾グループ
_bullets = bullets;
}
生成に必要なパラメータを受け取って設定を行っています。
アニメーションについて説明します。loadGraphic()の第2引数に true を指定すると画像がアニメーションのファイルとして扱われます。
そしてアニメーションの作成という部分で、連続したアニメを生成するようにしています。
// アニメーションを作成
this.animation.add("Default", [0, 1, 0, 2], Math.floor(6 + FlxRandom.float() * 4));
第1引数にアニメの名前、第2引数にアニメのパターン、第3引数にアニメの切り替え間隔を指定します。
アニメパターンの計算は画像の「横幅÷盾の高さ」で求められます。alien.pngの画像サイズは48x16なので、48/16=3
で自動的に3パターンとみなされます。第2引数が[0, 1, 0, 2]
なので、0 -> 1 -> 0 -> 2 の順番でアニメします。そしてその間隔は第3引数の処理で6〜10フレームのランダムな値となります。
あと、弾を撃つ間隔を決めるために resetShotClock()という関数を用意しています。
続いて、移動と弾を撃つ処理です。
/**
* 更新
**/
override public function update():Void {
if(x < _originalX - 8) {
// 左の移動最大値に到達
x = _originalX - 8;
// 反対方向に移動するようにする
velocity.x = -velocity.x;
// 下に移動する
velocity.y++;
}
if(x > _originalX + 8) {
// 右の移動最大値に到達
x = _originalX + 8;
velocity.x = -velocity.x;
}
if(y > FlxG.height * 0.35) {
// おおよそ画面の盾の高さ1/3より下にいれば、ショットタイマーを減らす
_shotClock -= FlxG.elapsed;
}
if(_shotClock <= 0) {
// 弾を撃つ
resetShotClock();
// 敵弾グループからインスタンスを取り出し
var bullet:FlxSprite = cast(_bullets.recycle(), FlxSprite);
// 位置を設定
bullet.reset(x + width - bullet.width/2, y);
// 下方向に移動する
bullet.velocity.y = 65;
}
super.update();
}
出現位置から8ピクセル左右に動きます。左端に移動した時下に1ピクセル移動しています。
弾を撃つ処理は、_shotClock
をデルタタイム(経過時間)ずつ減らして、それが0以下になったら弾を撃つようにしています。
敵の配置(PlayState.hx)
PlayState.hxに戻って、Alienクラスを配置していきます。
class PlayState extends FlxState {
private var _player:PlayerShip; // 自機
public var playerBullets:FlxGroup; // 自機の弾グループ
private var _aliens:FlxGroup; // エイリアングループ
private var _alienBullets:FlxGroup; // 敵弾グループ
override public function create():Void {
super.create();
// 自機を配置
// ... (省略)
// 敵弾の確保
var numAlienBullets:Int = 32;
_alienBullets = new FlxGroup(numAlienBullets);
for(i in 0...numAlienBullets) {
var sprite = new FlxSprite(-100, -100);
sprite.makeGraphic(2, 8);
sprite.exists = false;
_alienBullets.add(sprite);
}
// 敵弾グループを登録
add(_alienBullets);
// エイリアンの配置
var numAliens:Int = 50;
_aliens = new FlxGroup(numAliens);
var colors:Array<Int> = [
FlxColor.BLUE,
FlxColor.BLUE | FlxColor.GREEN,
FlxColor.GREEN,
FlxColor.GREEN | FlxColor.RED,
FlxColor.RED
];
// 10x5で配置
for(i in 0...numAliens) {
var a:Alien = new Alien(
8 + (i % 10) * 32, // X座標
24 + Std.int(i / 10) * 32, // Y座標
colors[Std.int(i / 10)], // 色
_alienBullets); // 敵弾グループを渡す
_aliens.add(a);
}
// エイリアングループを登録
add(_aliens);
}
まず敵弾を32発ぶん確保しています。そして敵を横10列、縦5行で配置しています。敵のインスタンスに敵弾グループを渡すことで、敵が弾を撃つ時に簡単にアクセスできるようになります。
実行すると、敵が出現して、移動しながら弾を撃ってくるようになります。
敵がアニメーションするのも確認できます。
シールドの配置(PlayState.hx)
インベーターといえばシールドの存在ですね。これを配置していきます。
class PlayState extends FlxState {
private var _shields:FlxGroup; // シールドグループ
override public function create():Void {
// 自機を配置
// ...(省略)
// シールドの配置
_shields = new FlxGroup();
for(i in 0...64) {
// 4x4のSpriteを、4x4で配置。4ブロック作る
var x:Int = 32 + 80 * Std.int(i / 16) + (i%4) * 4;
var y:Int = FlxG.height - 32 + (Std.int((i%16) / 4) * 4);
var sprite:FlxSprite = new FlxSprite(x, y);
sprite.makeGraphic(4, 4);
_shields.add(sprite);
}
add(_shields);
}
_shields
フィールドを追加して、インスタンスを登録していきます。シールドの配置座標の計算がややこしいですが、4x4単位で4ブロック配置しているだけとなります。
実行すると、シールドが配置されます。
当たり判定を実装
今回は当たり判定を複数のグループで行います。このような場合は当たり判定用のグループを作ると処理を最適化することができます。
- 自機の弾 -> 敵・シールドと衝突
- 敵の弾 -> 自機・シールドと衝突
class PlayState extends FlxState {
// ... (省略)
private var _vsPlayerBullets:FlxGroup; // 自機の弾と当たり判定をするグループ
private var _vsAlienBullets:FlxGroup; // 敵弾と当たり判定をするグループ
override public function create():Void {
super.create();
// 自機を配置
// ... (省略)
// 当たり判定グループを作成
// 自機の弾
_vsPlayerBullets = new FlxGroup();
_vsPlayerBullets.add(_aliens); // エイリアンが対象
_vsPlayerBullets.add(_shields); // シールドが対象
// 敵の弾
_vsAlienBullets = new FlxGroup();
_vsAlienBullets.add(_player); // プレイヤーが対象
_vsAlienBullets.add(_shields); // シールドが対象
}
当たり判定用のFlxGroupを作成して、そこにFlxGroupを追加するという階層化の構造となります。
そして、当たり判定の処理です。update()関数に当たり判定処理を追加します。FlxG.overlap()で当たり判定するオブジェクトと衝突後のコールバック関数を登録します。FlxG.overlap()はFlxG.collide()よりも柔軟な処理ができる関数です。ただここでは特別な処理をしていないので、collide()に置き換えても同じように動作します。
override public function update():Void {
super.update();
if (FlxG.keys.justPressed.ESCAPE) {
throw new Error("Terminate.");
}
// 自機の弾とそれに衝突するグループとの当たり判定を行う
FlxG.overlap(playerBullets, _vsPlayerBullets, _cbHitStuff);
// 敵弾とそれに衝突するグループとの当たり判定を行う
FlxG.overlap(_alienBullets, _vsAlienBullets, _cbHitStuff);
}
/**
* 当たり判定の処理
**/
private function _cbHitStuff(obj1:FlxObject, obj2:FlxObject) {
obj1.kill();
obj2.kill();
}
勝利と敗北の判定とメッセージ表示
最後に、勝利と敗北の判定とメッセージ表示を実装します。
class PlayState extends FlxState {
private static var statusMessage:String = "WELCOME TO FLX INVADERS"; // メッセージ
override public function create():Void {
super.create();
// 自機を配置
// ... (省略)
// タイトル・メッセージ作成
var t:FlxText = new FlxText(4, 4, FlxG.width - 8, statusMessage);
t.alignment = "center";
add(t);
}
メッセージ表示はFlxTextを使うとできます。あとメッセージ内容をstaticにすることで、Stateをリセットしても保持するようにします。
そしてupdate()に自機の死亡判定と、敵の全滅判定を実装します。
override public function update():Void {
super.update();
// ... (省略)
if(_player.exists == false) {
// プレイヤー死亡
statusMessage = "YOU LOST";
FlxG.resetState(); // リセット
}
else if(_aliens.getFirstExisting() == null) {
// 敵全滅
statusMessage = "YOU WON";
FlxG.resetState(); // リセット
}
}
FlxGroup.getFirstExisting()で保持しているインスタンスの先頭のアドレスを取得し、nullであれば全滅とみなします。