ライツアウト
Web上で動くものをお手軽に作りたくなりました。
クロスプラットフォームツールキットである Haxe で OpenFL というものを使うと、ゲームの様なメディアコンテンツが Windows, macOS, Linux, iOS, Android, Flash そして HTML5に出力できるそうです。
試しに有名なライツアウトを作ってみました。 さっそくHTML5で出力。
下記をクリックしてみて下さい。 Javascriptで動きます。 ※音はありません。
クリックしたタイルとその上下左右のタイルが反転(赤,緑)します。 すべてのタイルを緑にすればクリア。 ライツアウトは全てのライトを消灯させるものですが、とにかく全て緑にして下さい。
ソースと素材は下記で公開しています。
# 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 をコマ毎に差し替えるようです。 その様に修正しました。
0 件のコメント:
コメントを投稿