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

今年の10月末にニコ生の実験放送に自作ゲームを投稿できるようになった そうで、今回はそれについてまとめてみた記事です
この記事を読み終えた頃には、「AkashicEngine完全に理解した」って言えるようになります、たぶん

※本記事は グレンジ Advent Calendar 2018 22日目の記事です

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/にアクセスすると、サンプルが実行されます

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

エンティティ

エンティティとは、シーン上で描画されるオブジェクトのことで、以下のようなものがあります

コンストラクタ名 機能
FilledRect 単色で塗りつぶした矩形を描画する
Sprite 画像を描画する
FrameSprite 画像を分割してそれらの一つを描画する。自動的にアニメーションさせることができる
Label 単一行テキストを描画する
SystemLabel システムフォントでテキストを描画する
E 複数のエンティティをまとめる
Pane 複数のエンティティをまとめ、領域でクリッピングする

シーンにエンティティを配置するには、次の2つのステップが必要です

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

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

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

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

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

アセット

アセットの登録

akashic initを行ったディレクトリで、

akashic scan asset

のコマンドを実行するとアセットが game.json に自動で登録されます 登録されるのは以下のファイルです

  • script フォルダ下の .js , .json
  • image フォルダ下の .jpg , .png
  • audio フォルダ下の .aac , .ogg
  • text フォルダ下のテキストとして読み込むファイル

アセットの利用

画像・オーディオ・テキストアセットはシーン作成時に、assetIds プロパティにシーン内で利用するアセットIDを指定します

akashic scan asset で登録したアセットの assetId は基本的に拡張子なしのファイル名です
game.json の以下の箇所に記載されてます
1010.PNG

アセットを参照するときは、scene.assets["hoge"]といった感じで、scene.assets から assetsId をキーとして取得します

画像表示

画像表示はエンティティの Sprite を利用します

これだけでOKです
Spriteについての詳細は「 Sprite | @akashic/akashic-engine

オーディオ

ゲーム内で音を鳴らすには音ひとつごとに .ogg.aacのファイルが必要です
(様々な実行環境に対応するためらしい)
再生処理は一行だけです

オーディオアセットはデフォルトではループしない音として登録されます
ループさせる場合は game.json を以下のように変更します
0000.PNG BGMは音のループで実現させます

テキスト

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

文字列の表示

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

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

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

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

アセットの準備

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

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

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

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

スクリプトで説明します

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

入力イベント

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

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

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

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

シーン上での入力

evで扱えるプロパティはエンティティに触れる入力と同じです

ランキングモード対応

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

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

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

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

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

これだけでランキング対応が完了です
ランキングに関しての詳細は「 ニコニコ新市場対応コンテンツ作成ガイド

ニコニコ新市場に投稿

ニコニコ新市場対応コンテンツの投稿方法」に全部書いてありますが、抜粋して簡略に説明します

1. game.jsonのあるディレクトリで、次のコマンドを実行

akashic export  html --output <zipFileName> --atsumaru

注意点

  • zipファイルの展開後のサイズが 10MB 以下でないと申請が通らない
  • script, text下のアセットの文字コードUTF-8 にしないと文字化けする恐れがある

2. 投稿ページ でゲームを登録 ※ニコニコのログインが必要

注意点

  • ここで指定した「ゲーム名」「アイコン画像」「紹介文」はニコニコ新市場にも反映される
  • 「アイコン」のサイズは 160x160 以上、320x320 以下 (正方形推奨)
  • 「ゲーム表示サイズ」はゲームの解像度と同じ値の指定を推奨
  • 「公開」にしないとニコニコ新市場に申請できない

3. 内容保存後、マイページで投稿するゲーム中の「その他 > ニコニコ新市場に登録申請」を選択
4. 「ニコニコ新市場へ登録申請」というダイアログが表示されるので、「申請」ボタンを押す
5. 投稿対象のゲームに「ニコニコ新市場に申請済み」と表示されたら完了

おわりに

ここまで読んだ方は「AkashicEngine完全に理解した」とこの記事をシェアしつつ、つぶやいてOKです

実験放送対応の自作ゲームはまだ少ないので、暇つぶしにでも作ってみてはいかがでしょうか

リフレクションについてまとめた

最近 "Reflection" について調べていたので、それについて自分なりにまとめようと思います。

▼学習を兼ねて作ったもの

sonoichi-blog.hatenablog.com

※この記事はUnityゆるふわサマーアドベントカレンダー 2018 12日目の記事です。(C#なのでギリUnityということで...)

Reflectionとは

クラスを他のプログラムから利用できるようにするため、 プログラムやライブラリ中にはクラス名やメンバー名、それらのアクセスレベル等の情報が格納されています。 これらの情報はメタデータと呼ばれ、 プログラムの実行時にメタデータを取り出すための機能をリフレクション(reflection)と呼びます。

(プログラムが自分自身の情報を調べることができる機能なので、reflection(鏡映、反射)と呼ぶわけです。)

実行時型情報 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

 です。

アクセシビリティレベルに関係なくメンバをあれこれできるのは便利だけど危険、加えて処理も遅いようなので、デバッグ用途で使うことの多い機能だと思います。

なぜリフレクションは遅いのか | POSTD

MemberInfo

名前や宣言されたクラスなど、メンバとしての情報を持っています。

各 Info のクラスの基底クラスであり、MemberTypeによって変数なのか、関数なのかといったことが判別できます。

gist.github.com

MethodInfo

関数に関する様々な情報を持ちます。

関数の実行もできます。

gist.github.com

methodInfo.IsSpecialName は関数がコンストラクタ( .ctor )や自動実装プロパティ( プロパティの get/set メソッド( get_xxx, set_xxx )等 )であるときに true になります。 

ParametorInfo.HasDefaultValue は .Net 4.5以降で使用可能です。

virtual, abstruct はそれぞれ MethodInfo.IsVirtual, MethodInfo.IsAbstract で判定できますが、overrideされたかのフラグは取得できません。判定したいときは

var isOverride = methodInfo.GetBaseDefinition().DeclaringType != methodInfo.DeclaringType;

のようにすればできます。

FieldInfo

変数に関する様々な情報を持ちます。

値の変更もできます。

gist.github.com

 IsDefined(), GetCustomAttribute(), GetCustomAttributes() はMethodInfoでも使用可能です。

PropertyInfo 

プロパティに関する様々な情報を持ちます。

gist.github.com

GetAccesser(), GetGetMethod(), GetSetMethod() の第一引数( bool nonPublic )は publicでないアクセサを取得するかどうかです。

PropertyのアクセサビリティレベルはPropertyInfoからは判定できないので、アクセサから求めます。

GetProperty() のオーバーロードに Type を渡すものがあり、なんだろうと思ったのですが、インデクサを取得するためのものでした。

上記の例では次のようなインデクサを定義しています。

[System.Runtime.CompilerServices.IndexerName("Indexer")]
public int this[int x, int y]
{
    get { return _array[y * _width + x]; }
    set { _array[y * _width + x] = value; }
}
private int _array;
private int _width;

IndexerNameAttributeで名前を指定しない場合、名前は"Item"になります。

インデックサ this[] をリフレクションで取得する

ConstructorInfo

コンストラクタに関する様々な情報を持ちます。 

ConstructorInfoを利用してインスタンスの作成ができます。

gist.github.com

BindingFlags

各 Info 取得時に検索するメンバを指定するためのものです。

ここでは、主に扱われそうなものを紹介します。

  • Public : publicなメンバ
  • NonPublic : publicでないメンバ
  • Static : 静的なメンバ
  • Instance : 静的でないメンバ
  • DeclaredOnly : 基底クラスから継承したメンバを含まない

BindingFlags は enum ですが、それぞれが各ビットのフラグを表しており、ビット演算のようにして扱うことができます。

// publicかつ非staticな関数を全取得

var methods = typeof(TestClass).GetMethods(BindingFlags.Public | BindingFlags.Instance);

 最後に

リフレクションはどんなメンバにもアクセスでき、外部から関数の実行や値の変更ができる便利な機能です。

実行時に設定ファイル(ScriptableObject)の値を変更したり、クラスが保持する値を全て出力したり、PlantUML用のコードを出力したりと、色々なことができそうな気がします。

間違った認識や、紹介した以外で便利な機能などありましたら教えていただけると嬉しいです。

型の構造を出力する拡張メソッドを作った

指定した型の内容を全出力する拡張メソッドを作りました。

f:id:sonoichi-60:20180812090950p:plain

ソースコード

gist.github.com

導入方法

  1. 以下のリンク先へ進み、右上の "Download ZIP" からダウンロード

    型の構造を出力する拡張メソッド · GitHub

  2. 解答したファイルをAssets内に移動

使い方

System.Typeの拡張メソッド、OutputStructure()を呼ぶことで、型の構造情報を持った文字列を得られます。

Debug.Log(typeof(Time).OutputStructure());
Debug.Log(typeof(int).OutputStructure());

第一引数の declearOnly は親クラスのメンバを含むかどうかのフラグ、

第二引数の ignoreSpecialName は特殊な名前のメンバ(自動実装プロパティによって追加されるメンバ等)を表示するかどうかのフラグです

public static string OutputStructure(this Type type, bool declaredOnly = false, bool ignoreSpecialName = true)

補足

基底クラスの型の全取得は以下の処理を利用しました

【C#】指定した型の基底クラスの情報をすべて取得する拡張メソッド - コガネブログ

 

Timelineエディタを使いやすくする

 

Timelineエディタを少し使いやすくする方法の紹介です

 

Trackのアイコンを変更する

Assets/Gizmos下に ファイル名をPlayableTrackのクラス名にしたアイコン画像を配置する
https://forum.unity.com/threads/icon-of-custom-timeline-track.509565/

f:id:sonoichi-60:20180801010258p:plain

Gismos/TrackAssets/(トラック名) とやりたかったが認識されず...
Gizmos直下のみ?

 

Track作成メニューの階層化

TrackAssetのクラスが所属しているnamespaceに応じて自動的に階層化されます

f:id:sonoichi-60:20180801010418p:plain
Test.Hoge.Test3Track とした場合、
Test > Hoge > Test3Track ではなく、
Test.Hoge > Test3Track となるみたいです

 

Trackの色を変える

TrackAsset継承クラスに、以下のような属性を付与するだけです
引数は前から順に 0~1 の r, g, b です

[TrackColor(0.22f, 0.35f, 0.9f)]

f:id:sonoichi-60:20180801010337p:plain

Trackの左端とClip下端の色が指定した色になります
CameraやUI、Materialなどで分類し、色を付けるとわかりやすいと思います

 

ClipのInspecter表示変更

以下、enumに応じてInspenterに表示する値を変えるサンプルです

gist.github.com

f:id:sonoichi-60:20180801010357p:plain

TimelineでTweenを再生できるトラックを作った

TimelineでTweenが再生できたらいいなと思ったので作りました。

 

 

導入方法

  1. AssetStoreよりDOTweenをインポート
  2. 下のリンクよりダウンロード

    github.com

  3. パッケージをインポート

  4. TimelineウィンドウのTrack作成メニューにTweenPlayablesが追加されています

f:id:sonoichi-60:20180730091642p:plain

 

使い方

クリップごとに任意で開始値・終了値・イージングタイプを指定し、開始点から終了点にかけてアニメーションを行います。

クリップのブレンドも可能です。

パッケージに入っているTween用のBehaviour, Mixer, Clip, Trackの基底クラスを利用すれば、楽にTweenのカスタムトラックが作成できると思うので、よかったら使ってみてください。

LinQでも使える2次元配列

2次元配列でLinQを使ったら1次元配列扱いになって不便だったので、LinQでも(2次元座標が)使える2次元配列のクラスを作りました。

 

 

実装 

 

使い方

LinQで2次元配列の要素の2次元座標を取得できます。

以下は使用例です。

// 幅10, 高さ10の二次元配列を作成
var width = 10;
var height = 10;
var ary = new Array2D<int>( width, height );

// 各要素に0~2のランダムな値を代入
for( int y = 0; y < height; y++ ) {
    for( int x = 0; x < width; x++ ) {
        ary.Set( x, y, Random.Range( 0, 3 ) );
    }
}

// "xが0"の要素に3を代入

ary.Set( elem => elem.x == 0, 3 );

// "xが5,またはyが3" の要素を全取得
var ary2 = ary.Where( elem => elem.x == 5 || elem.y == 3 );

// "valueが1" の要素を全取得
var ary3 = ary.Where( elem => elem.value == 1 );

// "xが5,またはyが3" かつ "valueが1"の全要素に
foreach( var elem in ary2.Union(ary3) ) {
    // なんかする
}

 

参考記事

qiita.com

 

SceneManageWindow作成で気づいたこと&参考にした資料

初めてのUnityエディタ拡張の上で気づいたこと&SceneManageWindow作成の上で参考にした資料をまとめておきます。

 

 

気づいたこと

TextFieldの文字列が消えない

"TextFieldに文字を入力し、Buttonを押したら入力文字列をリセット"

っていうよく使いそうな処理をしたら奇妙な現象が起こりました。

f:id:sonoichi-60:20171113184807g:plain

TextFieldから文字が消えない…

上のウィンドウでは以下のような処理をしてます。

_text = EditorGUILayout.TextField( _text, EditorStyles.textField );
if( GUILayout.Button( "Log" ) ) {
    Debug.Log( _text );
    _text = "";
}

問題なのは、「TextFieldで入力された文字列は(内部でstringの値が変更されても)フォーカスされている間保持される」という点です。

つまり、適当なタイミングでTextFieldのフォーカスを外せばいいということでした。

以下のような処理でフォーカスを外すことができます。

GUI.FocusControll("");

GUIのフォーカスを外す - けいごのなんとか

 

 画面上のマウス座標の取得

パスを表示するポップアップを作る際、右クリックメニューのようにクリックした点にポップアップを出したかったので、画面上のマウス座標を取得する必要がありました。

まず、Unityでのゲーム制作でよく使われるInput.mousePositionですが、これはエディタ実行時にはGameViewを基準としたマウス座標を返し、非実行時には(0,0,0)を返すので使えず。

調べると、Event.current.mousePositionという、いかにもな値があったのですが、これは処理を記述したエディターウィンドウのGUI上のマウス座標みたいです。

Unity - スクリプトリファレンス: Event

画面上のマウス座標は以下のように取得しました。

var mouseGUIPos = Event.current.mousePosition;
var mouseScreenPos = EditorGUIUtility.GUIToScreenPoint( mouseGUIPos );

 

他、参考にした資料 

▼エディタ拡張入門

はじめに - エディター拡張入門

【エディタ拡張徹底解説】初級編①:ウィンドウを自作してみよう【Unity】 – ケットシーウェア

▼シーンの作成・削除・複製・名前変更

第26章 AssetDatabase - エディター拡張入門

Unity - マニュアル: AssetDatabase

▼開始シーンの設定

エディタ上で再生を開始するSceneを固定する【Unity】【エディタ拡張】 - (:3[kanのメモ帳]

 ▼マルチシーンを開く

楽にシーンを開く拡張機能を作ってみた(マルチシーン対応)【Unity】【エディタ拡張】 - (:3[kanのメモ帳]

▼ScenesInBuildの並び替え可能なリスト

第14章 ReorderbleList - エディター拡張入門

▼データ保存関連

第3章 データの保存 - エディター拡張入門

JsonUtility をつかって Unity で JSON を取り扱う方法 - Qiita

▼Path関連処理(ディレクトリ名の取得、使用不可文字の取得など)

Path クラス (System.IO)

▼シーン一覧の折りたたみ

Unity のエディタ拡張で FoldOut をかっこよくするのをやってみた - 凹みTips

▼仕切り線

エディタ拡張で仕切り線を描く - Qiita

▼ビルトインのGUIStyleを見る

Show Built In Resources - Unify Community Wiki

 

最後に

前回の記事のPV数が思ってたより伸びてて、いそいそしてます。

これからもぼちぼち更新していくのでよろしくお願いします。