Haxeでお手軽ゲーム製作 ライツアウト

ライツアウト

Web上で動くものをお手軽に作りたくなりました。

クロスプラットフォームツールキットである HaxeOpenFL というものを使うと、ゲームの様なメディアコンテンツが Windows, macOS, Linux, iOS, Android, Flash そして HTML5に出力できるそうです。

試しに有名なライツアウトを作ってみました。 さっそくHTML5で出力。

下記をクリックしてみて下さい。 Javascriptで動きます。 ※音はありません。

クリックしたタイルとその上下左右のタイルが反転(赤,緑)します。 すべてのタイルを緑にすればクリア。 ライツアウトは全てのライトを消灯させるものですが、とにかく全て緑にして下さい。

ソースと素材は下記で公開しています。

Bitbucket repository

# mercurial hg
hg clone https://bitbucket.org/hanepjiv/lightsout-hx

画像

六角形のタイルのアニメーションパターンを含む、3枚のみ。

Assets/
└── images
    ├── hex.png
    ├── clear.png
    └── background.png

ソース

ソースも3ファイルのみという簡潔さ。

Source/
├── Main.hx
└── lightsout
    ├── ClickedEvent.hx
    └── Hex.hx
Main.hx
package;

import openfl.Assets;
import openfl.Lib;
import openfl.display.Bitmap;
import openfl.display.BitmapData;
import openfl.display.Sprite;
import openfl.events.Event;
import openfl.events.MouseEvent;
import openfl.geom.Rectangle;
import openfl.geom.Point;


#if (!flash || enable_gamepad_support)
import openfl.events.GameInputEvent;
import openfl.ui.GameInput;
import openfl.ui.GameInputDevice;
import openfl.ui.GameInputControl;
#end

import lightsout.Hex;
import lightsout.ClickedEvent;

enum State {
  Start;
  Game;
  Clear;
  End;
}

class Main extends Sprite {
  private static var COLUMNS        = 3;
  private static var ROWS       = COLUMNS;
  private static var CLEAR_DURATION = 600;

  private var m_State:          State;
  private var m_CacheTime:      Int;
  private var m_LifeTime:       Int;

  private var m_Data_Background:    BitmapData;
  private var m_Data_Clear:     BitmapData;
  private var m_Data_Hex:       BitmapData;
  private var m_Data_Hexs:      Array<BitmapData>;

  private var m_Background:     Sprite;
  private var m_Hexs:           Array<Array<Hex>>;
  private var m_Clear:          Bitmap;

  public function new () {
    super ();

    m_State     = Start;
    m_CacheTime     = Lib.getTimer();
    m_LifeTime      = 0;

    m_Data_Background   = Assets.getBitmapData("assets/images/background.png");
    m_Data_Clear    = Assets.getBitmapData("assets/images/clear.png");
    m_Data_Hex      = Assets.getBitmapData("assets/images/hex.png");
    m_Data_Hexs     = new Array<BitmapData>();
    for (i in  0 ... 64) {
      m_Data_Hexs[i]    = new BitmapData(128, 128);
      m_Data_Hexs[i].copyPixels(m_Data_Hex,
                new Rectangle(128 * (i%8), 128 * Std.int(i/8),
                          128, 128),
                new Point(0, 0));
    }

    m_Background    = new Sprite();
    addChild(m_Background);

    m_Hexs       = new Array<Array<Hex>>();
    for (r in 0 ... ROWS) {
      m_Hexs[r]      = new Array<Hex>();
      for (c in 0 ... COLUMNS) {
    m_Hexs[r][c]         = new Hex(c, r, m_Data_Hexs);
    m_Hexs[r][c].x       = c * 128;
    m_Hexs[r][c].y       = r * 128;
    addChild(m_Hexs[r][c]);
      }
    }

    m_Clear = new Bitmap(m_Data_Clear, true);
    m_Clear.visible = false;
    addChild(m_Clear);

    resize(stage.stageWidth, stage.stageHeight);

    stage.addEventListener(Event.RESIZE, stage_onResize);
    addEventListener(MouseEvent.MOUSE_DOWN, this_onMouseDown);
    addEventListener(ClickedEvent.TYPED_CUSTOM_EVENT,
             this_onTypedClickedEvent);
    addEventListener(Event.ENTER_FRAME, this_onEnterFrame);

    init();
  }
  private function init() {
    for (row in 0 ... ROWS) { for (col in 0 ... COLUMNS) {
    m_Hexs[row][col].turn(Math.random() > 0.5);
      } }
    m_Clear.visible = false;
    m_State = Game;
  }
  private function resize(newWidth: Int, newHeight: Int):Void {
    m_Background.graphics.beginBitmapFill(m_Data_Background);
    m_Background.graphics.drawRect(0, 0, newWidth, newHeight);

    var m:Int = Std.int(Math.min(stage.stageWidth, stage.stageHeight));
    var x = 0;
    var y = 0;
    if (m < stage.stageWidth)   { x = Std.int((stage.stageWidth  - m) / 2); }
    if (m < stage.stageHeight)  { y = Std.int((stage.stageHeight - m) / 2); }
    var size = m / COLUMNS;
    for (row in 0 ... ROWS) { for (col in 0 ... COLUMNS) {
    m_Hexs[row][col].set_scale(size);
    m_Hexs[row][col].x = x + Std.int(col * size);
    m_Hexs[row][col].y = y + Std.int(row * size);
      } }

    m_Clear.x = Std.int((stage.stageWidth  - m_Clear.width) / 2);
    m_Clear.y = Std.int((stage.stageHeight - m_Clear.height) / 2);
  }
  private function stage_onResize(event: Event):Void {
    resize(stage.stageWidth, stage.stageHeight);
  }
  private function toggle(row: Int, col: Int) {
    if (row < 0 || ROWS <= row || col < 0 || COLUMNS <= col) { return; }
    m_Hexs[row][col].toggle();
  }
  private function this_onTypedClickedEvent(event: Event): Void {
    if (m_State != Game) { return; }
    if (!Std.is(event, ClickedEvent)) { return; }
    {  // toggle
      var custum:ClickedEvent = cast event;
      m_Hexs[custum.m_Row][custum.m_Col].toggle();
      toggle(custum.m_Row,    custum.m_Col + 1);
      toggle(custum.m_Row,    custum.m_Col - 1);
      toggle(custum.m_Row + 1,    custum.m_Col);
      toggle(custum.m_Row - 1,    custum.m_Col);
    }
    {  // clear check
      var isClear = true;
      for (row in 0 ... ROWS) { for (col in 0 ... COLUMNS) {
      isClear = isClear && m_Hexs[row][col].test();
    } }
      if (isClear) {
    m_State = Clear;
    m_LifeTime = 0;
    m_Clear.scaleX = m_Clear.scaleY = 0.0;
    m_Clear.visible = true;
      }
    }
  }
  private function this_onMouseDown(event: MouseEvent) {
    if (m_State != End) { return; }
    init();
  }
  private function this_onEnterFrame(event: Event): Void {
    var l_CurrentTime = Lib.getTimer();
    var l_Elapsed = l_CurrentTime - m_CacheTime;
    m_LifeTime += l_Elapsed;

    for (row in 0 ... ROWS) { for (col in 0 ... COLUMNS) {
    m_Hexs[row][col].onElapsed(l_Elapsed);
      } }

    switch m_State {
    case Clear: {
      m_Clear.scaleX =
          m_Clear.scaleY =
          (Math.min(m_Data_Clear.width, stage.stageWidth) /
           m_Data_Clear.width)
          * (m_LifeTime / CLEAR_DURATION);
      m_Clear.x = Std.int((stage.stageWidth  - m_Clear.width) / 2);
      m_Clear.y = Std.int((stage.stageHeight - m_Clear.height) / 2);
      if (CLEAR_DURATION < m_LifeTime) { m_State = End; }
    }
    default: {
    }
      }

    m_CacheTime = l_CurrentTime;
  }
}
Hex.hx
package lightsout;

import openfl.display.Bitmap;
import openfl.display.BitmapData;
import openfl.display.Sprite;
import openfl.events.MouseEvent;
import lightsout.ClickedEvent;

class Hex extends Sprite {
  private var m_Col:            Int;
  private var m_Row:            Int;
  private var m_OnOff:          Bool;
  private var m_Elapsed:        Int;
  private var m_Data_Hexs:      Array<BitmapData>;
  private var m_Bitmap:         Bitmap;

  public function new(a_Col: Int, a_Row: Int, a_Data_Hexs: Array<BitmapData>) {
    super();
    m_Col       = a_Col;
    m_Row       = a_Row;
    m_OnOff     = true;
    m_Elapsed   = 0;
    m_Data_Hexs = a_Data_Hexs;
    m_Bitmap    = new Bitmap(a_Data_Hexs[0], true);
    addChild(m_Bitmap);
    addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
  }
  private function onMouseDown(event: MouseEvent) {
    parent.dispatchEvent(new ClickedEvent(ClickedEvent.TYPED_CUSTOM_EVENT,
                                          m_Col, m_Row));
  }
  public function set_scale(size: Float) {
    scaleX = scaleY = size / Math.min(m_Data_Hexs[0].width,
                                      m_Data_Hexs[0].height);
  }
  public function turn(onoff:Bool) {
    m_OnOff     = onoff;
    m_Elapsed   = 0;
  }
  public function toggle() {
    turn(!m_OnOff);
  }
  public function test(): Bool {
    return m_OnOff;
  }
  public function onElapsed(l_Elapsed: Int): Void {
    m_Elapsed += l_Elapsed;
    var index = 0;
    if (!m_OnOff) { index = 32; }
    if (m_Elapsed < (16 * 32)) {
      index += Std.int(m_Elapsed/16);
    } else {
      index += 31;
    }
    m_Bitmap.bitmapData = m_Data_Hexs[index];
    m_Bitmap.smoothing = true;
  }
}
ClickedEvent.hx
package lightsout;

import openfl.events.Event;

class ClickedEvent extends Event {
  public static var TYPED_CUSTOM_EVENT = "typedClickedEvent";
  public var m_Col: Int;
  public var m_Row: Int;
  public function new(type:String, a_Col:Int, a_Row: Int,
      bubbles:Bool = false, cancelable:Bool = false) {
    super (type, bubbles, cancelable);
    this.m_Col = a_Col;
    this.m_Row = a_Row;
  }
  public override function clone(): ClickedEvent {
    return new ClickedEvent(type, m_Col, m_Row, bubbles, cancelable);
  }
  public override function toString ():String {
    return "[ClickedEvent type=\"" + type + "\" bubbles=" + bubbles +
    " cancelable=" + cancelable + " eventPhase=" + eventPhase +
    " col=" + m_Col + " row=" + m_Row + "]";
  }
}

おわり

とてもお手軽だと思います。

ただ。 スプライトアニメーションの実現に Sprite に全ての画像を addChild しておき、 現在のフレームで描画したいコマのみを可視にするという処理をしています。

これが正しいのか、いまいち自信がありません。 詳しい方、ぜひともご教示ください。

多くの場合 SpriteSheet というライブラリを用いるようですが、 今回はできるだけ軽量に済ませたかったため見送りました。

追記 2017/02/13

SpriteSheet を調べた所、 Bitmap を一つ追加して、その bitmapData をコマ毎に差し替えるようです。 その様に修正しました。