6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

第二のドワンゴAdvent Calendar 2019

Day 17

初心者によるTypeScriptを用いたAkashic Engine入門

Last updated at Posted at 2019-12-16

はじめに

この記事は, ドワンゴ Advent Calendar 2019 のおかわり版である 第二のドワンゴ Advent Calendar 2019 の参加記事です.
Akashic Engine を TypeScript で入門して, 「ゲームが作れる気がする!」という状態になるのが本記事の目的です.
制作したゲームは RPGアツマールに投稿して, みんなに遊んでもらうこともできます.
さらに, 登録申請を行うと, ニコニコ新市場対応コンテンツとして, ニコニコ生放送アプリで遊ぶこともできるようになります.

TypeScript による Akashic Engine 入門 v2版

JavaScript で書かれた Akashic Engine 入門 v2版 が非常に分かり易いので, これをTypeScriptでやってみようというのがここからの内容です.

TypeScript での型定義の利用

TypeScriptでゲームを開発する場合には, 型定義ファイルとして このリポジトリlib/main.d.ts を使うことができます.
npm install -D @akashic/akashic-engine でインストールの上, tsconfig.json で,
node_modules/@akashic/akashic-engine/lib/main.d.ts を参照するなどの方法で, tsc に与えてください.
今回の場合は, 標準で対応している TypeScript 用のテンプレートを利用していくので, 以上の準備は必要ありません.

akashic導入とテンプレートの利用

初めに akashic導入 を参考にして, akashic init コマンドを使えるようにしてください.
akashic init を利用すると, Akashic のゲームとして最低限動作するスクリプトやディレクトリを自動的に生成することができます.
標準で対応している TypeScript 用のテンプレートとしては,

  • typescript
  • typescript-minimal
  • typescript-shin-ichiba-ranking

があり, 今回は typescript-minimal を利用します. 作業ディレクトリに移動後, 以下のコマンドを実行してください.

akashic init -t typescript-minimal

width, height, fps はとりあえずデフォルトで設定しておきましょう.
上記コマンド実行後に, 色々生成されますが, 最初は ./src/main.ts に何かしていく流れになります.

後で説明するアンカーポイントを利用するために, package.jsondevDependencies 内を以下のように変更しましょう.

"@akashic/akashic-engine": "~2.4.11" → "@akashic/akashic-engine": "~2.6.1"
"@akashic/akashic-sandbox": "~0.13.55" → "@akashic/akashic-sandbox": "~0.15.21"

その後, 以下を実行します.

npm install

テンプレートを利用したことにより, 既に最低限動くものが出来ているので, 試しに実行してみましょう.

npm run build
npm start

実行後には, 左から右に移動していく正方形が表示されると思います.
以後, コードの実行は上記のコマンドにより行います.
tutorial1.gif
akashic-cli の利用方法は以下が参考になります.
akashic-cli 利用ガイド

コンテンツ作成の基本

シーンに画像や文字などのオブジェクト (以下, エンティティという. ) を描画して, コンテンツを作成していきます.
以上で実行した main.ts のmain関数の中を見ていきましょう.

main.ts
function main(param: g.GameMainParameterObject): void {
	const scene = new g.Scene({game: g.game}); // シーン作成
	scene.loaded.add(() => {
		// 以下にゲームのロジックを記述します.
		const rect = new g.FilledRect({
			scene: scene,
			cssColor: "#ff0000",
			width: 32,
			height: 32
		});
		rect.update.add(() => {
			// 以下のコードは毎フレーム実行されます.
			rect.x++;
			if (rect.x > g.game.width) rect.x = 0;
			rect.modified();
		});
		scene.append(rect);
	});
	g.game.pushScene(scene);
}

export = main;

ここでは, シーンを作成し, 読み込み完了後に行う処理を scene.loaded トリガーの add メソッドで加え, その後に g.gamepushScene メソッドでシーンの追加とシーン遷移の要求を行なっています.
今回の場合は, シーン読み込み後に g.FilledRect() で単色で塗りつぶした矩形エンティティを作成し, シーンに追加し, このエンティティに, x 座標を少しずつ増やし, 表示ゲーム幅を超えたら0に戻すという処理を毎フレーム行う処理として加えることで, 先ほどのような動作をする正方形を記述しています.

また, 各エンティティには, 様々なプロパティがあり, 自由に値を設定することが出来ます.
例えば, g.FilledRect() であれば, このリンクに示されるようなプロパティを設定できます.

上記のコードでは, cssColor width height の値のみが設定されていますが, x y プロパティの値を変更することで, 自由に四角形を描画することなども可能です.
多くのエンティティで共通の x y width height プロパティの関係を以下に簡単に示しました.

FilledrRect.png

このように, コンテンツを作る基本的な流れは以下のようになります.

  1. シーン作成
  2. 該当シーン上で実現したい処理を対応するトリガーに登録
    1. エンティティ作成
    2. 必要であれば, エンティティのトリガーに処理を登録
  3. シーンスタックにシーンを追加

以後, 特に指定がない限り, 本記事で示されるコードは, scene.loaded トリガーの add メソッドで加える関数として記述されるものとなります.

画像を扱う

Akashic Engine では PNG 形式と JPEG 形式の画像を扱えます.
テンプレート利用時に作成された image ディレクトリに適当な画像を加えましょう. 以下では, daifuku_mame.png を加えた場合の例を示しています.
追加した画像を Akashic Engine で利用するためには, game.json にアセット情報を追加する必要があります. テンプレートで生成されたディレクトリ構造の場合, 以下のコマンドを用いることで, アセット情報の追加を自動的に行うことが出来ます.

akashic scan asset

実際に game.json を確認すると, "assets" に追加した画像データの名前が追加されていることが分かります.
早速, 追加した画像を画面中心に表示してみましょう. エンティティとしては, Sprite を利用します.
以下のコードは, main 関数の中を示します.

main.ts
const scene = new g.Scene({
	game: g.game,
	assetIds: ["daifuku_mame"]
});
scene.loaded.add(() => {
	const daifukuAsset = scene.assets["daifuku_mame"] as g.ImageAsset;
	const daifuku = new g.Sprite({
		scene: scene,
		src: daifukuAsset,
		x: g.game.width / 2 - daifukuAsset.width / 2,
		y: g.game.height / 2 - daifukuAsset.height / 2
	});
	scene.append(daifuku);
});
g.game.pushScene(scene);

daifuku.png

シーン作成時に利用したいアセットIDを渡すことで, 対象のアセットをシーン内で利用できるようになります.
注意点として, TypeScriptの場合, scene.assets["hoge"]g.Asset として扱われてしまうため, このままでは画像アセットの幅や高さ情報を取得できません. そのため, 以上のようにして g.ImageAsset として利用するようにしてください.

エンティティの入れ子

エンティティには子エンティティを追加することができ, 複数のエンティティを一度に操作することができます.
複数のエンティティをグループ化する際に利用するエンティティとして E があります.
この E に属するエンティティは, E を基準とする相対座標系に位置します.
以下に, angle を設定した E に 3つの矩形を追加する例を示します.

main.ts
const group = new g.E({ scene: scene, x: 50, y: 50, angle: 30 });
const cssColors = ["darkgreen", "darkorange", "darkred"];
for (let i = 0; i < 3; i++) {
	const rect = new g.FilledRect({
		scene: scene,
		cssColor: cssColors[i],
		width: 30,
		height: 30,
		x: i * 30,
		y: 0
	});
	group.append(rect);
}
scene.append(group);

以上のように, 3つの矩形を E を用いてグループ化したことによって, E オブジェクトを操作することで 3 つの矩形エンティティを一度に操作することができるようになります.
上記プログラムの実行結果の座標関係は以下のようになっています.
E_example.png

アニメーションさせる

アニメーション

エンティティの位置や大きさ, 色などを表すプロパティをプログラムで変更させることで, エンティティをアニメーションさせることが可能です.
もし, 以上のようなプロパティを変更した場合, 明示的に modified() メソッドを呼び出し, 変更を通知する必要があるので, 忘れないようにしましょう.
テンプレートを読み込んだ後, 一番最初に実行したコードでも, rect.x を変更した後に rect.modified() を明示的に呼び出し, 変更を通知しています.

アニメーションさせる時に便利なライブラリもあるので, ぜひ利用しましょう.

以下に, 豆大福が下へ移動していき, ゲーム画面の半分を超えた瞬間に消えるアニメーションのプログラム例を示します.
実装にあたり, エンティティ自体の update トリガーを利用していて, フレームを描画するたびに登録ハンドラが呼び出されます.
エンティティ自体のトリガーを利用しているため, 対象のエンティティが destroy() メソッドなどで破棄された場合は, トリガーが無効になります.

main.ts
const daifuku = new g.Sprite({
	scene: scene,
	src: scene.assets["daifuku_mame"]
});
scene.append(daifuku);

// daifuku の update トリガーにハンドラを登録
daifuku.update.add(() => {
	daifuku.y++;
	daifuku.modified();

	// daifuku の y 座標がゲーム画面の半分を超えたら, daifuku 消去
	if (daifuku.y > g.game.height / 2) daifuku.destroy();
});

animation_example.gif

フレームアニメーション

エンティティ FrameSprite を利用すれば, フレームアニメーションを手軽に行うことができます.
複数のフレームがまとめられた一枚の画像を FrameSpritesrcWidth srcHeight に従って分割し, frames プロパティで左上から右下へインデックスを対応付けます.
以下の素材を利用したフレームアニメーションのプログラム例を示します.
sample_explosion.png

準備として, ダウンロードして得られた PNG ファイルを image ディレクトリに配置し, akashic scan asset をしましょう.

main.ts
const explosion = new g.FrameSprite({
	scene: scene,
	src: scene.assets["sample_explosion"] as g.ImageAsset,

	// エンティティのサイズ
	width: 100,
	height: 100,

	// 元画像の1フレームのサイズ
	srcWidth: 100,
	srcHeight: 100,

	// アニメーションに利用するフレームのインデックス配列
	// インデックスは元画像の左上から右にsrcWidthとsrcHeightの矩形を並べて数え上げ, 右端に達したら一段下の左端から右下に達するまで繰り返す
	frames: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],

	// アニメーションをループする(省略した場合ループする)
	loop: true
});
scene.append(explosion);
explosion.start();

framesprite_example.gif
FrameSprite エンティティのパラメータオブジェクトのプロパティには, 最初に表示するフレームを指定する frameNumber やフレーム遷移の速度を指定する interval を指定することができます. start() メソッドを呼び出すまでアニメーションは開始されません. アニメーションを終了したい場合は, stop() メソッドを呼び出します.

注意点として, TypeScriptの場合, Sprite と同様に, src プロパティに scene.assets["hoge"] だけを指定しても上手くいかず, g.ImageAsset とする必要があります.

タイマー関数

シーンオブジェクトの scene.setTimeout() メソッドを利用すると, 一定時間後に一度だけ実行される処理を作成することができます.
また, scene.setInterval() メソッドを利用すると, 一定間隔で定期的に実行される処理を作成することができます.
加えて, scene.clearTimeout(), scene.clearInterval() を利用することで, 登録した関数を解除することができます.

以下に, scene.setInterval() で登録した関数を 3 秒後に scene.clearInterval() で解除するプログラム例を示します.

main.ts
const daifuku = new g.Sprite({
	scene: scene,
	src: scene.assets["daifuku_mame"]
});
scene.append(daifuku);

const intervalId = scene.setInterval(() => {
	daifuku.y += 10;
	daifuku.modified();
}, 200);

scene.setTimeout(() => {
	scene.clearInterval(intervalId);
}, 3000);

setinterval_example.gif

乱数

Akashic Engine は独自の乱数生成器を備えています. Akashic Engine に用意された乱数生成器を利用すると, 指定した範囲の整数をランダムに生成できます. 以下の式は __0 以上 9 以下__の整数をランダムに一つ選んで返します.

g.game.random.get(0, 9)

また, チュートリアルでは言及されていないようですが, akashic-engine@2.5.1 から, JavaScript 標準関数 Math.random() 互換の g.Game#random#generate() が追加されています.

注意点として, Akashic ゲームでは 一部例外を除き, 基本的に JavaScript 標準関数 Math.random() を利用しないようにしてください.
詳しい理由などについては, よくある落とし穴 を参考にしてください.

以下に, 4種類の色の中からランダムに矩形の色を選び, 格子状にそれらの矩形並べるプログラム例を示します.

main.ts
const size = 25;
const margin = 15;
const colors = ["blue", "navy", "royalblue", "skyblue"];

for (let y = 0; y < g.game.height; y += size + margin) {
	for (let x = 0; x < g.game.width; x += size + margin) {
		const idx = g.game.random.get(0, colors.length - 1);
		const rect = new g.FilledRect({
			scene: scene,
			x: x,
			y: y,
			width: size,
			height: size,
			cssColor: colors[idx]
		});
		scene.append(rect);
	}
}

random_sample.png

クリックできるようにする

エンティティの touchable プロパティを true にすることで, ゲーム画面のタップやクリックなどのユーザ操作を検出できるようになります. Akashic Engine では, このようなユーザ操作を検出すると, ポイントイベントを発生します.

エンティティ対象のポイントイベントには, 以下の3種類が存在します.
pointevent.jpg
上記のイベントに対応するトリガーを利用することで, 指定したイベントが発生した場合に, 処理を実行できます.

シーンオブジェクトに対して, 以下のポイントイベントのトリガーを利用した場合, シーン全体でポイントイベントを受け取ることができます.
シーンオブジェクト対象のポイントイベントには, 以下の3種類が存在します.
pointcapture.jpg
ポイントイベントを受け取れる状態にある複数のエンティティが重なり合っている場合は, 最前面に表示されているエンティティがポイントイベントを受け取ります.

以下に, クリックされる度に色がレッド, グレーに変化する矩形を表示するプログラム例を示します.

main.ts
let idx = 0;
const colors = ["red", "gray"];
const rect = new g.FilledRect({
	scene: scene,
	cssColor: colors[idx],
	width: 50,
	height: 50,
	touchable: true
});
scene.append(rect);

rect.pointDown.add(() => {
	idx = (idx + 1) % 2;
	rect.cssColor = colors[idx];
	rect.modified();
});

click_example.gif

また, ポイントイベントが発生した座標を取得することも可能です.
取得するためには, トリガーに渡す関数に引数を追加します.
以下に, 画面に指が触れた時, もしくはマウスのボタンが押された時に, その座標に矩形エンティティを配置するプログラム例を示します.

main.ts
scene.pointDownCapture.add((ev) => {
	const size = 20;
	const rect = new g.FilledRect({
		scene: scene,
		x: ev.point.x - size / 2,
		y: ev.point.y - size / 2,
		width: size,
		height: size,
		cssColor: "blue"
	});
	scene.append(rect);
});

scene_click_example.gif

音を鳴らす

以下が参考になります.
音を鳴らす

拡縮とアンカーポイント

拡縮

エンティティの scaleX scaleY プロパティを利用して拡大・縮小, angle プロパティを利用して回転を行うことができます.
以下に, 元のサイズよりも横方向に 3 倍, 縦方向に 1.5 倍拡大して, さらに 45° 回転した状態で豆大福を描画するプログラム例を示します.

main.ts
const daifukuAsset = scene.assets["daifuku_mame"] as g.ImageAsset;
const daifuku = new g.Sprite({
	scene: scene,
	src: daifukuAsset,
	x: g.game.width / 2 - daifukuAsset.width / 2,
	y: g.game.height / 2 - daifukuAsset.height / 2,
	scaleX: 3,
	scaleY: 1.5,
	angle: 45
});
scene.append(daifuku);

extended_daifuku.png

アンカーポイント

通常, エンティティの拡大, 縮小, 回転はエンティティの中心を基準とし, 位置 x y はエンティティの左上を基準とします.
エンティティのアンカーポイントを指定することで, この両方を統一して設定できます.
anchorX anchorY では 0.0 がエンティティの左端, 上端を, 1.0 がエンティティの右端, 下端を表します.

以下に, アンカーポイントを用いて, 上記と同様な結果を得るプログラム例を示します.

main.ts
const daifukuAsset = scene.assets["daifuku_mame"] as g.ImageAsset;
const daifuku = new g.Sprite({
	scene: scene,
	src: daifukuAsset,
	x: g.game.width / 2,
	y: g.game.height / 2,
	scaleX: 3,
	scaleY: 1.5,
	angle: 45,
	anchorX: 0.5, //akashic-engine@2.5.4 以降 (akashic-sandbox@0.15.11, akashic-cli@1.7.28 以降)
	anchorY: 0.5
});
scene.append(daifuku);

anchorX anchorY0.5 にすることで, アセットの中心を基準に設定しています.
プログラム中では x y プロパティでゲーム画面の中心を指定し, 先ほどのように画像アセットのサイズの半分を減算する必要がなくなっている点がポイントです.
anchor_info.png

色々な描画

文字列の表示

Akashic Engine で文字列を表示する際には, フォントラベル が必要です.
フォントは文字の形を表すオブジェクトであり, ラベルはフォントを利用して文字列を描画するエンティティになります.
フォントには, BitmapFontDynamicFont の 2種類 があります.
BitmapFont は画像から生成され, DynamicFont はシステムにインストールされているフォントから生成されます.

ラベルには, Label エンティティを利用します.

DynamicFont

以下に DynamicFont を用いて, Hello World!するプログラム例を示します.

main.ts
const font = new g.DynamicFont({
	game: g.game,
	fontFamily: g.FontFamily.SansSerif,
	size: 15
});

const label = new g.Label({
	scene: scene,
	font: font,
	text: "Hello World!",
	fontSize: 15,
	textColor: "blue",
	x: g.game.width / 2,
	y: g.game.height / 2,
	anchorX: 0.5,
	anchorY: 0.5
});
scene.append(label);

hello.png
色やサイズだけでなく, DynamicFont のプロパティで 太字 にしたり, Label のプロパティで中央揃えにすることも可能です.
また, より高級なテキスト描画が行えるようになる便利なライブリ akashic-label などもあります.

BitmapFont

BitmapFont を用いて, Hello World!するプログラム例を示します.
素材としては, サンプルデモの素材のビットマップフォントを利用しています.
上記リンクでダウンロードした font16_1.pngimage に, glyph_area_16.jsontext に配置し, akashic scan asset を実行し, scene の assetIdsfont16_1, glyph_area_16 を追加しましょう.

main.ts
const glyphText = scene.assets["glyph_area_16"] as g.TextAsset;
const glyph = JSON.parse(glyphText.data);

const font = new g.BitmapFont({
	src: scene.assets["font16_1"],
	map: glyph,
	defaultGlyphWidth: 16,
	defaultGlyphHeight: 16
});

const label = new g.Label({
	scene: scene,
	font: font,
	text: "Hello World!",
	fontSize: 16,
	x: g.game.width / 2,
	y: g.game.height / 2,
	anchorX: 0.5,
	anchorY: 0.5
});
scene.append(label);

bitmapfont_sample.png
TypeScriptの場合, g.TextAssetdata を取得するには, 上記のようにする必要があることに注意しましょう.
ビットマップフォントを準備する際には, ttf ファイルからビットマップ画像と json ファイルを生成できる bmpfont-generatorが便利です.

テキストの変更

ラベルの描画内容はキャッシュされているため, text font fontSize プロパティを変更した場合には, invalidate() メソッドで変更を通知する必要があります.

以下に, 500ミリ秒ごとにカウンタの値を増やすプログラム例を示します.

main.ts
const font = new g.DynamicFont({
	game: g.game,
	fontFamily: g.FontFamily.SansSerif,
	size: 15
});

let count = 0;
const counter = new g.Label({
	scene: scene,
	font: font,
	text: count + "",
	fontSize: 30,
	textColor: "black",
	x: g.game.width / 2,
	y: g.game.height / 2,
	anchorX: 0.5,
	anchorY: 0.5
});
scene.append(counter);

scene.setInterval(() => {
	counter.text = ++count + "";
	counter.invalidate();
}, 500);

counter_example.gif

クリッピング

Pane は, E と同様に, エンティティをグループ化するためのエンティティです.
E と異なり, Pane は 子孫要素の描画領域をその Pane の大きさに限定し, 領域をはみ出した部分については, 描画が行われません.
つまり, Pane は枠を表すエンティティに相当します.

また, PaneLabel と同様に, 描画内容をキャッシュしているため, Pane のプロパティを変更した場合には, invalidate() で変更を通知する必要があります.
一方で, Pane の子孫要素の変更時には, その子孫要素の modified() invalidate() を呼び出せばよく, 自動的に Pane に変更が通知され, キャッシュが更新されます.

以下に, Pane 内に配置した矩形を回転させるプログラム例を示します.

main.ts
const pane = new g.Pane({ scene: scene, width: 50, height: 50 });
const rect = new g.FilledRect({
	scene: scene,
	width: 50,
	height: 50,
	x: 15,
	y: 15,
	angle: 30,
	cssColor: "red"
});
pane.append(rect);
scene.append(pane);

rect.update.add(() => {
	++rect.angle;
	rect.modified(); // Pane は何も変更されていないため, rect の変更だけ通知すればよい.
});

pane_example.gif

さらなる情報

Akashic Engine におけるシーン遷移, ファイル分割を行う方法は以下にまとめられています.
より大規模なゲームを作るために

また, より詳細な情報は以下にまとめられています.
コンテンツ作成手順に従って, 実際にゲームを作ってみるといいかもしれません.
また, コンテンツ例に Akashic Engine を用いたゲームがまとめられているので, とても参考になります.
Akashic Engine 関連情報一覧

マルチプレイゲーム

Akashic Engineを用いれば, 簡単にマルチプレイゲームを作ることができます.
以下のリンクが非常に参考になります.

ゲームを公開する

以下のリンクが参考になります. ぜひ, 何か作って投稿してみましょう.

6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?