はじめに
この記事は, ドワンゴ 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.json
の devDependencies
内を以下のように変更しましょう.
"@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
実行後には, 左から右に移動していく正方形が表示されると思います.
以後, コードの実行は上記のコマンドにより行います.
akashic-cli の利用方法は以下が参考になります.
akashic-cli 利用ガイド
コンテンツ作成の基本
シーンに画像や文字などのオブジェクト (以下, エンティティという. ) を描画して, コンテンツを作成していきます.
以上で実行した main.ts
のmain関数の中を見ていきましょう.
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.game
の pushScene
メソッドでシーンの追加とシーン遷移の要求を行なっています.
今回の場合は, シーン読み込み後に g.FilledRect()
で単色で塗りつぶした矩形エンティティを作成し, シーンに追加し, このエンティティに, x 座標を少しずつ増やし, 表示ゲーム幅を超えたら0に戻すという処理を毎フレーム行う処理として加えることで, 先ほどのような動作をする正方形を記述しています.
また, 各エンティティには, 様々なプロパティがあり, 自由に値を設定することが出来ます.
例えば, g.FilledRect()
であれば, このリンクに示されるようなプロパティを設定できます.
上記のコードでは, cssColor
width
height
の値のみが設定されていますが, x
y
プロパティの値を変更することで, 自由に四角形を描画することなども可能です.
多くのエンティティで共通の x
y
width
height
プロパティの関係を以下に簡単に示しました.
このように, コンテンツを作る基本的な流れは以下のようになります.
- シーン作成
-
該当シーン上で実現したい処理を対応するトリガーに登録
- エンティティ作成
- 必要であれば, エンティティのトリガーに処理を登録
- シーンスタックにシーンを追加
以後, 特に指定がない限り, 本記事で示されるコードは, scene.loaded
トリガーの add
メソッドで加える関数として記述されるものとなります.
画像を扱う
Akashic Engine では PNG 形式と JPEG 形式の画像を扱えます.
テンプレート利用時に作成された image
ディレクトリに適当な画像を加えましょう. 以下では, daifuku_mame.png を加えた場合の例を示しています.
追加した画像を Akashic Engine で利用するためには, game.json
にアセット情報を追加する必要があります. テンプレートで生成されたディレクトリ構造の場合, 以下のコマンドを用いることで, アセット情報の追加を自動的に行うことが出来ます.
akashic scan asset
実際に game.json
を確認すると, "assets"
に追加した画像データの名前が追加されていることが分かります.
早速, 追加した画像を画面中心に表示してみましょう. エンティティとしては, Sprite
を利用します.
以下のコードは, main 関数の中を示します.
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);
シーン作成時に利用したいアセットIDを渡すことで, 対象のアセットをシーン内で利用できるようになります.
注意点として, TypeScriptの場合, scene.assets["hoge"]
は g.Asset
として扱われてしまうため, このままでは画像アセットの幅や高さ情報を取得できません. そのため, 以上のようにして g.ImageAsset
として利用するようにしてください.
エンティティの入れ子
エンティティには子エンティティを追加することができ, 複数のエンティティを一度に操作することができます.
複数のエンティティをグループ化する際に利用するエンティティとして E
があります.
この E
に属するエンティティは, E
を基準とする相対座標系に位置します.
以下に, angle
を設定した E
に 3つの矩形を追加する例を示します.
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 つの矩形エンティティを一度に操作することができるようになります.
上記プログラムの実行結果の座標関係は以下のようになっています.
アニメーションさせる
アニメーション
エンティティの位置や大きさ, 色などを表すプロパティをプログラムで変更させることで, エンティティをアニメーションさせることが可能です.
もし, 以上のようなプロパティを変更した場合, 明示的に modified()
メソッドを呼び出し, 変更を通知する必要があるので, 忘れないようにしましょう.
テンプレートを読み込んだ後, 一番最初に実行したコードでも, rect.x
を変更した後に rect.modified()
を明示的に呼び出し, 変更を通知しています.
アニメーションさせる時に便利なライブラリもあるので, ぜひ利用しましょう.
以下に, 豆大福が下へ移動していき, ゲーム画面の半分を超えた瞬間に消えるアニメーションのプログラム例を示します.
実装にあたり, エンティティ自体の update
トリガーを利用していて, フレームを描画するたびに登録ハンドラが呼び出されます.
エンティティ自体のトリガーを利用しているため, 対象のエンティティが destroy()
メソッドなどで破棄された場合は, トリガーが無効になります.
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();
});
フレームアニメーション
エンティティ FrameSprite
を利用すれば, フレームアニメーションを手軽に行うことができます.
複数のフレームがまとめられた一枚の画像を FrameSprite
の srcWidth
srcHeight
に従って分割し, frames
プロパティで左上から右下へインデックスを対応付けます.
以下の素材を利用したフレームアニメーションのプログラム例を示します.
準備として, ダウンロードして得られた PNG ファイルを image
ディレクトリに配置し, akashic scan asset
をしましょう.
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
エンティティのパラメータオブジェクトのプロパティには, 最初に表示するフレームを指定する 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()
で解除するプログラム例を示します.
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);
乱数
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種類の色の中からランダムに矩形の色を選び, 格子状にそれらの矩形並べるプログラム例を示します.
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);
}
}
クリックできるようにする
エンティティの touchable
プロパティを true
にすることで, ゲーム画面のタップやクリックなどのユーザ操作を検出できるようになります. Akashic Engine では, このようなユーザ操作を検出すると, ポイントイベントを発生します.
エンティティ対象のポイントイベントには, 以下の3種類が存在します.
上記のイベントに対応するトリガーを利用することで, 指定したイベントが発生した場合に, 処理を実行できます.
シーンオブジェクトに対して, 以下のポイントイベントのトリガーを利用した場合, シーン全体でポイントイベントを受け取ることができます.
シーンオブジェクト対象のポイントイベントには, 以下の3種類が存在します.
ポイントイベントを受け取れる状態にある複数のエンティティが重なり合っている場合は, 最前面に表示されているエンティティがポイントイベントを受け取ります.
以下に, クリックされる度に色がレッド, グレーに変化する矩形を表示するプログラム例を示します.
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();
});
また, ポイントイベントが発生した座標を取得することも可能です.
取得するためには, トリガーに渡す関数に引数を追加します.
以下に, 画面に指が触れた時, もしくはマウスのボタンが押された時に, その座標に矩形エンティティを配置するプログラム例を示します.
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);
});
音を鳴らす
以下が参考になります.
音を鳴らす
拡縮とアンカーポイント
拡縮
エンティティの scaleX
scaleY
プロパティを利用して拡大・縮小, angle
プロパティを利用して回転を行うことができます.
以下に, 元のサイズよりも横方向に 3 倍, 縦方向に 1.5 倍拡大して, さらに 45° 回転した状態で豆大福を描画するプログラム例を示します.
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);
アンカーポイント
通常, エンティティの拡大, 縮小, 回転はエンティティの中心を基準とし, 位置 x
y
はエンティティの左上を基準とします.
エンティティのアンカーポイントを指定することで, この両方を統一して設定できます.
anchorX
anchorY
では 0.0
がエンティティの左端, 上端を, 1.0
がエンティティの右端, 下端を表します.
以下に, アンカーポイントを用いて, 上記と同様な結果を得るプログラム例を示します.
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
anchorY
を 0.5
にすることで, アセットの中心を基準に設定しています.
プログラム中では x
y
プロパティでゲーム画面の中心を指定し, 先ほどのように画像アセットのサイズの半分を減算する必要がなくなっている点がポイントです.
色々な描画
文字列の表示
Akashic Engine で文字列を表示する際には, フォント と ラベル が必要です.
フォントは文字の形を表すオブジェクトであり, ラベルはフォントを利用して文字列を描画するエンティティになります.
フォントには, BitmapFont
と DynamicFont
の 2種類 があります.
BitmapFont
は画像から生成され, DynamicFont
はシステムにインストールされているフォントから生成されます.
ラベルには, Label
エンティティを利用します.
DynamicFont
以下に DynamicFont
を用いて, Hello World!するプログラム例を示します.
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);
色やサイズだけでなく, DynamicFont のプロパティで 太字 にしたり, Label のプロパティで中央揃えにすることも可能です.
また, より高級なテキスト描画が行えるようになる便利なライブリ akashic-label などもあります.
BitmapFont
BitmapFont
を用いて, Hello World!するプログラム例を示します.
素材としては, サンプルデモの素材のビットマップフォントを利用しています.
上記リンクでダウンロードした font16_1.png
を image
に, glyph_area_16.json
を text
に配置し, akashic scan asset
を実行し, scene の assetIds
に font16_1
, glyph_area_16
を追加しましょう.
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);
TypeScriptの場合, g.TextAsset
の data
を取得するには, 上記のようにする必要があることに注意しましょう.
ビットマップフォントを準備する際には, ttf ファイルからビットマップ画像と json ファイルを生成できる bmpfont-generatorが便利です.
テキストの変更
ラベルの描画内容はキャッシュされているため, text
font
fontSize
プロパティを変更した場合には, invalidate()
メソッドで変更を通知する必要があります.
以下に, 500ミリ秒ごとにカウンタの値を増やすプログラム例を示します.
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);
クリッピング
Pane
は, E
と同様に, エンティティをグループ化するためのエンティティです.
E
と異なり, Pane
は 子孫要素の描画領域をその Pane
の大きさに限定し, 領域をはみ出した部分については, 描画が行われません.
つまり, Pane
は枠を表すエンティティに相当します.
また, Pane
は Label
と同様に, 描画内容をキャッシュしているため, Pane
のプロパティを変更した場合には, invalidate()
で変更を通知する必要があります.
一方で, Pane
の子孫要素の変更時には, その子孫要素の modified()
invalidate()
を呼び出せばよく, 自動的に Pane
に変更が通知され, キャッシュが更新されます.
以下に, Pane
内に配置した矩形を回転させるプログラム例を示します.
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 の変更だけ通知すればよい.
});
さらなる情報
Akashic Engine におけるシーン遷移, ファイル分割を行う方法は以下にまとめられています.
より大規模なゲームを作るために
また, より詳細な情報は以下にまとめられています.
コンテンツ作成手順に従って, 実際にゲームを作ってみるといいかもしれません.
また, コンテンツ例に Akashic Engine を用いたゲームがまとめられているので, とても参考になります.
Akashic Engine 関連情報一覧
マルチプレイゲーム
Akashic Engineを用いれば, 簡単にマルチプレイゲームを作ることができます.
以下のリンクが非常に参考になります.
ゲームを公開する
以下のリンクが参考になります. ぜひ, 何か作って投稿してみましょう.