LoginSignup
11
7

More than 5 years have passed since last update.

【AkashicEngine完全に理解した】環境構築からニコニコにゲームを投稿するまで

Last updated at Posted at 2018-12-21

グレンジ Advent Calendar 2018 22日目担当の sono160 です
グレンジでクライアントエンジニアをやってます
ニコ生が好きです
今年の10月末にニコ生の実験放送に自作ゲームを投稿できるようになった そうで、今回はそれについてまとめてみた記事です
この記事を読み終えた頃には、「AkashicEngine完全に理解した」って言えるようになります、たぶん

AkashicEngineとは

このページ を見ればわかりますが、自分なりにまとめると
「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/にアクセスすると、サンプルが実行されます

sample.gif

各ファイル・フォルダについて

ファイル・フォルダ名 説明
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利用ガイド

ゲームの構成

akashic_constructure.png
ゲーム内に複数のシーンが存在し、シーンにエンティティ(シーン上に描画されるオブジェクト)を配置してゲームを構成します
エンティティは親子関係を持つことができ、動的に生成、破棄することができます
以下、サンプルコードにコメントを追加したものです

main.js
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つのステップが必要です

  1. new 演算子でエンティティオブジェクトを作る
  2. シーンの append() メソッドでエンティティオブジェクトをシーンに追加する

※「 コンテンツ作成の基本 」より引用

ここでは全てのエンティティやそのプロパティについては説明しないので、気になったら上の表中の各"コンストラクタ名"をクリックしてください(公式リファレンスに飛びます)

エンティティができることの一部を、手っ取り早くスクリプトで説明します

main.js
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;

実行すると以下のようになります
sample2.gif

アセット

アセットの登録

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 の以下の箇所に記載されてます
1010.PNG

アセットを参照するときは、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 を以下のように変更します
0000.PNG
BGMは音のループで実現させます

テキスト

テキストは以下のようにしてテキストデータを取得できます

// text.txt のテキストを取得
var text = scene.assets["text"].data;
// data.json からデータを取得
var data = JSON.parse(scene.assets["data"].data);

文字列の表示

文字列の表示方法は、次の二種類があります

  • DynamycFontSystemLabel エンティティを利用して表示
  • BitmapFontLabel エンティティを利用して表示

ここでは後者の方法を説明します

前者は "サンセリフ体(ゴシック)", "セリフ体(明朝体)", "等幅フォント" の3種類が使えますが、安っぽい見た目になってしまうので説明は省略します。
気になる方は 「 色々な描画

アセットの準備

ビットマップフォントの表示には "フォント(jpg/png)" とそれに対応した "グリフ(json)" のアセットが必要です
ここではAkashicEngineの サンプルデモの素材からダウンロードしたものを使います

展開したファイルの font16_1.pngimage フォルダに、glyph_area_16.jsontext フォルダに移します
ファイルの中身を見るとわかると思いますが、
フォント画像 = "同じ大きさのフォントを並べた画像"、グリフ = "文字に対応したテクスチャ座標(左上)が記述されているjson"
といった感じです

アセットをフォルダに移したら akashic scan asset でアセットを登録します

ビットマップフォントの描画処理

スクリプトで説明します

main.js
// シーン作成 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);
}

これで以下のように文字列が表示されます
11112.PNG

入力イベント

"触れた", "移動した", "離した"といった入力を取得するイベント(ポイントイベント)は以下の二種類があります

  • エンティティに触れる入力
  • シーン上での入力

それぞれスクリプトで説明していきます

エンティティに触れる入力

main.js
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で扱えるプロパティはエンティティに触れる入力と同じです

main.js
scene.pointDownCapture.add (function(ev) {
    // タッチされたときの処理
    // シーンの入力の場合、原点はゲーム画面左上
});
scene.pointMoveCapture.add (function(ev) {
    // 画面に触れたままタッチ座標が移動した時の処理
});
scene.pointUpCapture.add (function(ev) {
    // 画面から指が離れた時の処理
});

ランキングモード対応

game.jsonenvironment に以下のように追記することで、ランキングモードのゲームとして扱われます
111111.PNG

ランキングモード対応のゲームは以下の条件を満たす必要があります

  • 一人プレイ
  • 一定時間でのスコアを競うゲーム
  • 0 ~ 99999 点のスコアを特定の変数に代入する

"特定の変数" とは g.game.vars.gameState.score のことで、ここに代入した値がスコアとしてランキングに利用されます
また、変数 g.game.vars.gameState.playThreshold にプレイ閾値を代入することで、そのスコア以下のプレイヤーを未プレイとみなし、ランキングから除外できます

main.js
// 変数を定義 (スコア0は未プレイとみなす)
g.game.vars.gameState = {
    score: 0,
    playThreshold: 0
}
// クリックするごとにスコア +1
scene.pointDownCapture.add(function() {
    g.game.vars.gameState.score ++;
});

ランキングモード時の残り時間はゲーム開始直後にサーバから通知される "セッションパラメータ" から受け取ります
以下のようにして受け取ることができます

main.js
// ランキングモードでないときの制限時間
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)が本記事を紹介してくださいました

気になる方はフォローしてみてください

11
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
11
7