グレンジ Advent Calendar 2018 22日目担当の sono160 です
グレンジでクライアントエンジニアをやってます
ニコ生が好きです
今年の10月末にニコ生の実験放送に自作ゲームを投稿できるようになった そうで、今回はそれについてまとめてみた記事です
この記事を読み終えた頃には、**「AkashicEngine完全に理解した」**って言えるようになります、たぶん
#AkashicEngineとは
[このページ] (https://dwango.github.io/articles/akashic/) を見ればわかりますが、自分なりにまとめると
**「iOSでもAndroidでもブラウザでも同様に快適に動き、多数の視聴者が同時にランキング対戦できる2Dゲームをjavascriptで簡単に作れるゲームエンジン」**です
おまけにツールも作れるらしいです
実際にどんなゲームがあるか気になった方は**「つりっくま」**で検索してみてください
実験放送で体感9割は遊ばれてる覇権ゲーです
#環境構築~サンプル実行まで
node.js のインストール
ここからインストール(推奨版でOK)
homebrew で入れる場合は下記の記事を参考
【2018年版】macのhomebrewでnodebrew入れてからnode.jsを入れるまで
AkashieEngine のインストール
ターミナル(コマンドプロンプト) に以下のコマンドを入力
npm install -g @akashic/akashic-sandbox
npm install -g @akashic/akashic-cli
ここで npm ERR! Error: EACCES: permission denied, ~~
みたいなエラーが出た方は下記の記事を参考(自分がなりました)
npmでpermission deniedになった時の対処法[mac]
プロジェクトの作成
プロジェクトを作成したいフォルダに移動し、以下のコマンドを入力
akashic init
すると、以下のように解像度とFPSの初期設定を聞かれるので、それぞれ入力してEnter
prompt: width: (320) 640
prompt: height: (320) 360
prompt: fps: (30)
解像度はニコ生の解像度と同じ 16:9 が推奨されています
入力すると、現在のディレクトリ下に必要なファイルが生成されます
サンプル実行
akashic init
を行ったディレクトリで、
akashic-sandbox
と入力します
これでゲーム用のローカルサーバが起動しているので、http://localhost:3000/
にアクセスすると、サンプルが実行されます
各ファイル・フォルダについて
ファイル・フォルダ名 | 説明 |
---|---|
audio | サウンドファイルを格納するフォルダ |
image | 画像ファイルを格納するフォルダ |
script | スクリプトファイルを格納するフォルダ |
script/main.js | ゲームのエントリポイント。最初は赤い四角が右に流れるだけのサンプルコードが書かれてる |
.elintrc.json | ESLint (JSの性的検証ツール) の設定ファイル。気になる方は「 ESLint 最初の一歩 」 |
game.json | 解像度やFPS、各種アセットの情報等、さまざまな情報を記載するjson。詳細は「 game.jsonの仕様 」 |
package.json | npmのモジュールに関数する情報。気になる方は「 package.jsonの中身を理解する 」 |
README.md | マークダウンで書かれたreadme |
audio , image , text フォルダ内には、必要なフォルダがgitで無視されないようにするために .gitkeep ファイルが入ってます |
|
詳細については次のページの最下部に記載されています(少し情報が古い) | |
akashic-cli利用ガイド |
#ゲームの構成
ゲーム内に複数のシーンが存在し、シーンにエンティティ(シーン上に描画されるオブジェクト)を配置してゲームを構成します
エンティティは親子関係を持つことができ、動的に生成、破棄することができます
以下、サンプルコードにコメントを追加したものです
function main(param) {
// シーン作成
var scene = new g.Scene({game: g.game});
// シーンロード完了時処理
scene.loaded.add(function() {
// rect エンティティの作成
var rect = new g.FilledRect({
scene: scene,
cssColor: "#ff0000",
width: 32,
height: 32
});
// rect エンティティの更新処理
rect.update.add(function () {
rect.x++;
if (rect.x > g.game.width) rect.x = 0;
rect.modified();
});
// rect エンティティをシーンに追加
scene.append(rect);
});
// ゲームにシーンを登録
g.game.pushScene(scene);
}
module.exports = main;
#エンティティ
エンティティとは、シーン上で描画されるオブジェクトのことで、以下のようなものがあります
コンストラクタ名 | 機能 |
---|---|
FilledRect | 単色で塗りつぶした矩形を描画する |
Sprite | 画像を描画する |
FrameSprite | 画像を分割してそれらの一つを描画する。自動的にアニメーションさせることができる |
Label | 単一行テキストを描画する |
SystemLabel | システムフォントでテキストを描画する |
E | 複数のエンティティをまとめる |
Pane | 複数のエンティティをまとめ、領域でクリッピングする |
シーンにエンティティを配置するには、次の2つのステップが必要です
-
new
演算子でエンティティオブジェクトを作る - シーンの
append()
メソッドでエンティティオブジェクトをシーンに追加する
※「 コンテンツ作成の基本 」より引用
ここでは全てのエンティティやそのプロパティについては説明しないので、気になったら上の表中の各"コンストラクタ名"をクリックしてください(公式リファレンスに飛びます)
エンティティができることの一部を、手っ取り早くスクリプトで説明します
function main(param) {
// シーン作成
var scene = new g.Scene({game: g.game});
// シーンロード完了時処理
scene.loaded.add(function() {
// 画面中央に空のエンティティを作成
var parent = new g.E({scene: scene, x: g.game.width/2, y: g.game.height/2});
// 変数 angle を初期値 0 で定義
// エンティティに変数を追加するときは tag を利用する
parent.tag = { angle: 0 };
// 矩形を作成して返す関数の定義
function createRect(cssColor) {
return new g.FilledRect({ scene: scene, cssColor: cssColor, width: 32, height: 32 });
}
// child1 を赤色(#ff0000)で作成
var child1 = createRect("#ff0000");
// child1 を parent の 子にする
// このとき child1 の左上がparentの座標(画面中心)になっている
// 他のエンティティに append() したエンティティはシーンに append() する必要はない
parent.append(child1);
// child1 を左下にずらす
child1.moveTo(-32, 0);
// child2(青)はparentの子にして右上に配置
var child2 = createRect("0000ff");
parent.append(child2);
child2.moveTo(0, -32);
// child3(緑)はparentの子にして中央に配置
// 後に追加されたものが前面に描画されるので、child3は最前面に描画される
var child3 = createRect("00ff00");
parent.append(child3);
child3.moveTo(-16, -16);
// parent更新処理
parent.update.add(function () {
// 毎フレーム5ずつ増加
parent.tag.angle += 5;
// parent の角度を変える
parent.angle = parent.tag.angle;
// parent に値の更新を通知する
parent.modified();
// 180度回転ごとに処理を分岐
switch(Math.floor(parent.tag.angle / 180)) {
// child1が表示されているなら child1 を非表示
case 1: if(child1.visible()) child1.hide(); break;
// child2 が破棄されていないなら child2 を削除
// child1 が表示されていないなら child1 を表示
case 2: if(!child2.destroyed()) child2.destroy();
if(!child1.visible()) child1.show(); break;
// child1 の透明度が1なら child1 を半透明に
case 3: if(child1.opacity == 1) { child1.opacity = 0.5; child1.modified(); } break;
// parent を削除
// 子の child1, child2 も同時に削除され、次の parent.update は呼ばれなくなる
case 4: parent.destroy(); break;
}
});
// parent をシーンに追加
scene.append(parent);
});
// シーンをゲームに登録
g.game.pushScene(scene);
}
module.exports = main;
#アセット
アセットの登録
akashic init
を行ったディレクトリで、
akashic scan asset
のコマンドを実行するとアセットが game.json
に自動で登録されます
登録されるのは以下のファイルです
-
script
フォルダ下の.js
,.json
-
image
フォルダ下の.jpg
,.png
-
audio
フォルダ下の.aac
,.ogg
-
text
フォルダ下のテキストとして読み込むファイル
アセットの利用
画像・オーディオ・テキストアセットはシーン作成時に、assetIds
プロパティにシーン内で利用するアセットIDを指定します
// "hoge.jpg", "hogehoge.jpg" のアセットを利用する
var scene = new g.Scene({game: g.game, assetIds: ["hoge", "hogehoge"]});
akashic scan asset
で登録したアセットの assetId
は基本的に拡張子なしのファイル名です
game.json
の以下の箇所に記載されてます
アセットを参照するときは、scene.assets["hoge"]
といった感じで、scene.assets
から assetsId
をキーとして取得します
画像表示
画像表示はエンティティの Sprite
を利用します
// "hoge.jpg" のスプライトを作成
var sprite = new g.Sprite({scene: scene, src: scene.assets["hoge"]});
// シーンに追加
scene.append(sprite);
これだけでOKです
Spriteについての詳細は「 Sprite | @akashic/akashic-engine 」
オーディオ
ゲーム内で音を鳴らすには音ひとつごとに .ogg
と .aac
のファイルが必要です
(様々な実行環境に対応するためらしい)
再生処理は一行だけです
// "sound"を再生
scene.assets["sound"].play();
オーディオアセットはデフォルトではループしない音として登録されます
ループさせる場合は game.json
を以下のように変更します
BGMは音のループで実現させます
テキスト
テキストは以下のようにしてテキストデータを取得できます
// text.txt のテキストを取得
var text = scene.assets["text"].data;
// data.json からデータを取得
var data = JSON.parse(scene.assets["data"].data);
#文字列の表示
文字列の表示方法は、次の二種類があります
-
DynamycFont
とSystemLabel
エンティティを利用して表示 -
BitmapFont
とLabel
エンティティを利用して表示
ここでは後者の方法を説明します
前者は "サンセリフ体(ゴシック)", "セリフ体(明朝体)", "等幅フォント" の3種類が使えますが、安っぽい見た目になってしまうので説明は省略します。
気になる方は 「 色々な描画 」
アセットの準備
ビットマップフォントの表示には "フォント(jpg/png)" とそれに対応した "グリフ(json)" のアセットが必要です
ここではAkashicEngineの [サンプルデモの素材] (https://akashic-games.github.io/asset/material.html)からダウンロードしたものを使います
展開したファイルの font16_1.png
を image
フォルダに、glyph_area_16.json
を text
フォルダに移します
ファイルの中身を見るとわかると思いますが、
フォント画像 = "同じ大きさのフォントを並べた画像"、グリフ = "文字に対応したテクスチャ座標(左上)が記述されているjson"
といった感じです
アセットをフォルダに移したら akashic scan asset
でアセットを登録します
ビットマップフォントの描画処理
スクリプトで説明します
// シーン作成 font16_1 と glyph_area_16 のアセットを使う
var scene = new g.Scene({
game: g.game,
assetIds: ["font16_1", "glyph_area_16"]
});
// シーンロード完了時処理
scene.loaded.add(function() {
// glyphの作成
var glyph = JSON.parse(scene.assets["glyph_area_16"].data);
// フォントの作成
// defaultGlyphWidth, defaultGlyphHeightに文字あたりの幅,高さを指定
var font = new g.BitmapFont({
src: scene.assets["font16_1"],
map: glyph,
defaultGlyphWidth: 16,
defaultGlyphHeight: 16
});
// 画面中央にフォントサイズ32のラベルを作成
var label = new g.Label({
scene: scene,
font: font,
fontSize: 32,
text: "1234567890",
x: g.game.width/2-32*5,
y: g.game.height/2-32/2
});
// ラベル登録
scene.append(label);
}
#入力イベント
"触れた", "移動した", "離した"といった入力を取得するイベント(ポイントイベント)は以下の二種類があります
- エンティティに触れる入力
- シーン上での入力
それぞれスクリプトで説明していきます
エンティティに触れる入力
var rect = new g.FilledRect({scene: scene, cssColor: "#ff0000", width: 32, height: 32 });
// タッチ可能なオブジェクトである
rect.touchable = true;
rect.pointDown.add (function(ev){
// rect がタッチされた時の処理
// ev.point.x, ev.point.y にタッチ座標(原点=エンティティの座標)が格納されている
});
rect.pointMove.add (function(ev){
// rect.pointDown から画面に触れたままでタッチ座標が移動した時の処理
// ev.point利用可能
// ev.startDelta.x, ev.startDelta.y に pointDown 時の座標からの移動量が格納されている
// ev.prevDelta.x, ev.prevDelta.y に 直前の pointMove 時の座標からの移動量が格納されている
});
rect.pointUp.add (function(ev){
// rect.pointDown 後に画面から指が離れた時の処理
// ev.point, ev.startDelta, ev.prevDelta利用可能
});
scene.append(rect);
シーン上での入力
ev
で扱えるプロパティはエンティティに触れる入力と同じです
scene.pointDownCapture.add (function(ev) {
// タッチされたときの処理
// シーンの入力の場合、原点はゲーム画面左上
});
scene.pointMoveCapture.add (function(ev) {
// 画面に触れたままタッチ座標が移動した時の処理
});
scene.pointUpCapture.add (function(ev) {
// 画面から指が離れた時の処理
});
#ランキングモード対応
game.json
の environment
に以下のように追記することで、ランキングモードのゲームとして扱われます
ランキングモード対応のゲームは以下の条件を満たす必要があります
- 一人プレイ
- 一定時間でのスコアを競うゲーム
- 0 ~ 99999 点のスコアを特定の変数に代入する
"特定の変数" とは g.game.vars.gameState.score
のことで、ここに代入した値がスコアとしてランキングに利用されます
また、変数 g.game.vars.gameState.playThreshold
にプレイ閾値を代入することで、そのスコア以下のプレイヤーを未プレイとみなし、ランキングから除外できます
// 変数を定義 (スコア0は未プレイとみなす)
g.game.vars.gameState = {
score: 0,
playThreshold: 0
}
// クリックするごとにスコア +1
scene.pointDownCapture.add(function() {
g.game.vars.gameState.score ++;
});
ランキングモード時の残り時間はゲーム開始直後にサーバから通知される "セッションパラメータ" から受け取ります
以下のようにして受け取ることができます
// ランキングモードでないときの制限時間
var totalLimitTime = 60;
// メッセージ受け取り処理
scene.message.add(function(msg) {
// 開始時のメッセージ かつ 合計制限時間のパラメータをもっていれば取得ZZ
totalLimitTime = msg.data.parameters.totalTimeLimit;
}
});
// シーンの更新処理
scene.update.add(function() {
// カウントダウン
totalLimitTime -= 1 / 30;
});
これだけでランキング対応が完了です
ランキングに関しての詳細は「 ニコニコ新市場対応コンテンツ作成ガイド 」
#ニコニコ新市場に投稿
「 ニコニコ新市場対応コンテンツの投稿方法」に全部書いてありますが、抜粋して簡略に説明します
1. game.jsonのあるディレクトリで、次のコマンドを実行
akashic export html --output <zipFileName> --atsumaru
注意点
- zipファイルの展開後のサイズが
10MB
以下でないと申請が通らない -
script
,text
下のアセットの文字コードをUTF-8
にしないと文字化けする恐れがある
2. 投稿ページ でゲームを登録 ※ニコニコのログインが必要
注意点
- ここで指定した「ゲーム名」「アイコン画像」「紹介文」はニコニコ新市場にも反映される
- 「アイコン」のサイズは
160x160
以上、320x320
以下 (正方形推奨) - 「ゲーム表示サイズ」はゲームの解像度と同じ値の指定を推奨
- 「公開」にしないとニコニコ新市場に申請できない
3. 内容保存後、マイページで投稿するゲーム中の「その他 > ニコニコ新市場に登録申請」を選択
4. 「ニコニコ新市場へ登録申請」というダイアログが表示されるので、「申請」ボタンを押す
5. 投稿対象のゲームに「ニコニコ新市場に申請済み」と表示されたら完了
#おわりに
ここまで読んだ方は**「AkashicEngine完全に理解した」**とこの記事をシェアしつつ、つぶやいてOKです
実験放送対応の自作ゲームはまだ少ないので、暇つぶしにでも作ってみてはいかがでしょうか
追記(12/25)
akashic engine の公式情報アカウント(@akashic_talk)が本記事を紹介してくださいました
【お役立ち情報】
— akashic_talk (@akashic_talk) 2018年12月25日
ユーザーの方がAkashic Engineについてまとめたページを公開してくださいました!
Akashicの環境構築から新市場への投稿に至るまでを1ページにまとめてわかりやすく紹介してくれています。是非参考にしてみてください。
⇒https://t.co/dwKLHtVgXV#コンテンツ制作講座
気になる方はフォローしてみてください