isobe_yakiのブログ

ニコ生ゲーム開発者向けの記事を書きます

3Dのニコ生ゲームを作ろう その1

ニコ生ゲームで3Dができたら面白そうではないか?ということでパート1では3Dゲームを作るための技術についてまとめてみる。

技術を調べる

1.標準機能だけでやる

Primitive Speed ~ 3Dドライビング ~というゲームが恐らくAkashicEngineのデフォの機能だけで作っている。

こちらのゲームは3Dで表現された街並みの中などを高速でかっ飛ばす1人称レースゲームだ。3Dの景観は実際には2D矩形描画を組み合わせて表現されている。ロール方向の視点回転まであるのでなかなか臨場感があるが、Akashicの標準機能だけで十分作れるはず*1

2.拡張ライブラリを使う

森のかくれんぼというゲームは恐らく公式?で出してるライブラリGitHub - akashic-games/raycaster-jsを使ってると思う。要はjsで3Dレンダラ書いちゃおうというもの。 この拡張ライブラリは使ったことないが割と高機能っぽくてビルボードとか距離フォグとか深度バッファも実装されている。木や草はビルボードを使っているし視界から遠ざかるほど暗くなるのには距離フォグを使っている。 ちなみにRenderer._putImageData()という命令を使えば、任意のビットマップをSurfaceに反映可能なので、このライブラリを使わず0からレイトレレンダラを自作することも可能だ。それであれば、理論的にはライティングや影、GI表現など好きな表現を盛り込んだレンダラにカスタマイズ可能であるが、JSが数値計算苦手なことを考えるとなかなか現実解にはなりにくい。 この拡張ライブラリ自体はそこそこな速度で動くようだが、ライブラリにせよ自作にせよスマホで動かすことなども考えるとソフトウェアレンダラでは表現力とパフォーマンスのトレードオフがかなり重要であり、表現に対して強い制約となってしまう。

3.その他?

実際にゲームを公開しているか不明だが、技術的に3Dにチャレンジしている方が見つかったので一応掲載。

qiita.com

qiita.com

qiita.com

みんなAkashicの制約の中で作っててえらい!というか2019なのでこのころはiOSのWebGL2対応もかなりダメダメだったはず、iOSユーザーを除外するような手段は記事にできんってことなのかも。

4.CanvasAPIを使う

isobe-yaki.hateblo.jp

前回の記事で取り上げた通り、ニコ生ゲームでもCanvasAPIを使うことができ、WebGLゲームも作れそうという話をした。そこで、今回は普通にWebGLを使ってニコ生ゲームを作ってみようと思う。

ちなみにWebGLにはバージョン1.0と2.0があり、2.0は機能的にかなり進化している。というか1.0がかなりパフォーマンスに問題があるので基本的には2.0を使うことになると思う。ただし、iOSはセキュリティ上の問題やブランディング戦略でWebGL対応がかなり遅かったらしく今*2でも2.0をサポートしてない端末はそこそこ出回ってそうであるが無視する。

しかし、WebGLどころかOpenGLプログラミングもほぼしたことないので何から始めたらいいのかわからない。本当にニコ生ゲームでWebGLが使えるかどうかの検証もしたい。調べてみたところWebで3Dコンテンツを作るならThree.jsというライブラリがメジャーらしい。

threejs.org

このライブラリはjs/tsから利用することができ、WebGLの煩雑な処理をカプセル化してオブジェクト指向的に3Dシーンやオブジェクトを扱える。数学計算や様々な3Dモデルのローダーも含まれており、いきなりからがっつり3Dコンテンツ開発ができるライブラリのようだ。というわけでまず、Three.jsをニコ生ゲームに組み込んで動くか動作検証を行った。検証したのはもう1年以上前でソースコードも残っていないため記憶の断片だが、確か球体にポイントライト1灯を当てただけのシーンを作ったと思う。Three.jsは単にgame.jsonに含めただけだとエクスポートでエラーが出てしまったので、色々コードをいじったり削ったりしながらもなんとか組み込み実験は成功し、ニコ生での起動、Androidでの表示まで確認できた。なので、一応これを使えばガチの3Dゲームは作れそうである。(パフォーマンス未検証)

と、ここまでやっておいてなんだが、自分の場合は結局Three.jsは使わなかった。 正直使いにくさを感じたのとゴリゴリの3Dゲームをjsで作るのは勘弁願いたかったので、ここから先はc++OpenGL SDKを使って3DエンジンからフルスクラッチしつつemscriptenでWebAssembly化することにした。なによりブラウザのデバッガがストレスだったので普段はwindowsゲームとして開発して時々Web出力して確認みたいなことがしたかったのだ。要はUnityみたいな仕組みを自前で用意しようということだ。この件については後々書こうと思うが、とりあえず次章ではWebGLをAkashic上で実際に使ってみようと思う。

*1:にしてもこのゲーム、コースがなかなか作りこまれている。どうやってコースデータ作成したのか気になるなぁ

*2:2023年6月

Canvasを使ってニコ生ゲームを作ろう

キャンバスの詳細は以下から↓ developer.mozilla.org

AkashicEngineでは内部でキャンバスを使っているが、何らかの目論見から隠蔽されており、実際にはかなり制限された描画命令しか使えないようになっている。

そういう厳しい制約の中で工夫してゲームを作るから面白いんじゃないか!という人もいるとは思うがプレイヤーには関係ないしやっぱり表現力が欲しい場面もあるだろう。 そこで、ブラウザの秘められた力を解放すべくキャンバスを直接使ったゲームを作ってみよう。

AkashicEngineの描画について

矩形描画しかサポートしてない!

AkashicEngineは描画バックエンドとして2Dと3D両方使える。game.jsonにあるrenderersという設定項目で指定できるようになっているとのこと。

つまりキャンバスへの描画にはCanvasRenderingContext2DWebGLRenderingContextのどちらかが使われることになる。しかし、MDNのリファレンスを見れば分かると思うがこの2つのAPIは全く互換が無くほとんど共通化ができない。共通化できそうなのはせいぜいライン描画と矩形描画くらいである。そのためAkashicでは矩形描画しかサポートしなかったのではないかと思われる。*1

g.Rendererインタフェース

AkashicEngineで画像を描画したい場合通常はg.Spriteクラスを使うと思うが、それ以外にRendererとSurfaceを使う方法がある。

g.Rendererを取得するにはg.game.renderersから取得するかg.Eを継承してrenderSelfを実装し、引数として受け取る方法がある。

取得したRendererのRenderer.drawImageを呼び出して描画したいSurfaceオブジェクトを渡せばよい。

g.Surfaceを作成するにはg.game.resourceFactory.createSurface(width, height)を使う。

以下は100*100のSurfaceを作成し、キャンバスAPIで模様を描画したものをRendererで表示するサンプルコードである。

const canvas = g.game.resourceFactory.createSurface(100, 100);
const ctx = canvas.context()._context;    // !ここハック
ctx.fillStyle = 'magenta';
ctx.fillRect(0, 0, 100, 100);
ctx.fillStyle = 'yellow';
ctx.beginPath();
ctx.arc(50, 50, 40, 0, Math.PI * 2);
ctx.fill();
// Surfaceを原点に描画するだけのクラス
class Canvas extends g.E {
    constructor(surface, params) {
        super(params);
        this.surface = surface;
    }

    renderSelf(renderer) {
        renderer.drawImage(this.surface, 0, 0, this.surface.width, this.surface.height, 0, 0);
    }
}
new Canvas(canvas, { scene, parent: scene, local: true });

上記コードの結果

ここではg.Eを継承したが拡大回転なども行うならg.Spriteを継承した方が便利なのでそちらもおススメ。その場合はSpriteのコンストラクタに渡すパラメータのsrcにSurfaceインスタンスを指定すればよい。

パネワンでの使用例

1.パネル画像

See the Pen ブロック by z0ero (@z0ero) on CodePen.

パネルの画像は単純なパス描画で立体感のある矩形を描画した後、テキスト描画を利用して図柄を表示している。ただし、テキスト描画は環境ごとに見た目が変わる可能性があるのでほんとはあまり推奨しない。

2.ランクボード

See the Pen ランクボード by z0ero (@z0ero) on CodePen.

成績発表の背景画像もキャンバスで生成している。arcToを駆使して丸角矩形のパスを作成し、createLinearGradientでグラデーション塗りつぶし+線の太さを変えて3回strokeすることで実現している。グラデーション塗りつぶしのような高度な2D描画機能が使えるのもキャンバスの恩恵である。

3.背景エフェクト

See the Pen Untitled by z0ero (@z0ero) on CodePen.

ここまでの応用でアニメーションも実装できる。応用といっても単にキャンバスを毎フレーム再描画するだけなので割と簡単である。ただ、落とし穴としてg.game.modified();を毎フレーム呼んでないとrenderSelfが呼ばれなくてアニメーションしないという不具合があったのでその辺だけ注意が必要。ちなみにこの背景エフェクトでは結構テクいこともしている。例えば各パーティクルの移動速度計算が重くならないように関数近似で軽量化を行ったり(どういう計算かは忘れた)、コンポジットモードをいじることで光の軌跡を残すようにしたりしている*2

広がるアイデア

パネワンでは限定的にしかキャンバスを活用しなかったが、ぶっちゃけAkashicの描画機能を一切使わず、全描画をキャンバスで行うことも可能だ。そこで既存の発想にとらわれないアイデアを考えてみよう!

1.お絵かきゲーム

ユーザーが描いた絵を共有して遊べるゲームだ。お題で絵を描いたり、描いた絵を戦わせたりするゲームなどが既に存在しているが、そういったゲームの場合、点や線などを大量のエンティティで表現するよりはキャンバスを使ってしまう方が自然だろう。頑張ればマルチで絵チャなんかも作れると思う。

2.3Dゲーム

canvas要素は3D描画も可能である。WebGLの知識が必要になるが、Three.jsなどのライブラリを利用すれば比較的簡単に3D表現もできるのではないだろうか。

3.ベクターアニメーション

AkashicEngineでは実験機能だが3.2.0以降でSVG画像も使えるようになっている。しかしあくまで静止画として利用可能なだけで、滑らかに変形するアニメーションなどは多分できない。キャンバスAPIなら高度な図形描画機能が豊富なのでフニャフニャと変形するような動きのキャラクターなども比較的まともな速度で表現できるのではなかろうか。

See the Pen ベクターキャラモーション by z0ero (@z0ero) on CodePen.

まとめ

  • g.Spriteなどを継承してrenderSelfをオーバーライドすることで独自にキャンバス描画した画像を普通のエンティティとしてAkashic上で表示できる
  • キャンバスAPIの強力な2D描画や3D描画機能が使えるのでブラウザゲーム相当の表現が可能である
  • よほど特殊機能でない限りブラウザやニコ生アプリ上で問題なく使える

*1:とは言えバックエンドにwebglが使われてるところは無さそうだし、2dならほぼ全てのブラウザが対応している状況で敢えてwebglが選択されるようなケースがあるのかは疑問である。

*2:しかも環境によってはサポートしていないモードもあるのでその対策も入れている

ニコ生ゲーにフォントを埋め込んでお洒落にする

ビットマップフォント

AkashicEngineにはBitmapFont | Akashic Engineというものがある。あらかじめ画像化したフォントとその画像のどのエリアがどの文字なのか対応付けたjsonをコンストラクタに渡すことで使えるようになる。

公式で配ってるビットマップフォントの画像
多分ほとんどのゲームがこれで個性的なUIを作っていると思う。でも、1つ問題があって、この方式だと文字サイズを動的に変更できなかったり、縁取り指定が出来なかったり、ユーザー名のような想定外の文字の表示には使えない(or厳しい)。

ダイナミックフォント

ビットマップフォントで対応しきれない問題を解決するにはやはりDynamicFont | Akashic Engineだろう。こちらはシステムにインストールされたフォントを使って動的にサイズや縁取りを変更してあらゆる文字がレンダリングできる。事前に必要な文字を考慮する必要が無い。

非常に便利なのだがこれもこれで問題があり、使えるフォントがユーザー環境依存なのだ。画像とか動画編集良くするよって人なら有名どころのフォントもインストールされているだろうが、大抵のユーザー特にスマホ勢などは基本的なフォントしか使えないだろう。そのためAkashicのリファレンスにも次のように書かれている。

フォント名として指定できる値は環境に依存する。 少なくとも "sans-serif", "serif", "monospace" (それぞれサンセリフ体、セリフ体、等幅の字体) は有効な値である。

Webフォント

webフォント - Google 検索 Web界隈詳しくないんでよく知らないけど、なんかWeb上にフォントが公開されてて、ページデザインとかに自由に使っていいよーみたいなのをWebフォントというらしい。

@font-face {
  font-family: "Open Sans";
  src:
    url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"),
    url("/fonts/OpenSans-Regular-webfont.woff") format("woff");
}

こんな感じでcssの中にフォントファイルのURLを指定するだけでいいので、自分でフォントを配布する必要もユーザーのシステムにフォントをインストールする必要もない。

これをダイナミックフォントに使えたらめちゃくちゃ便利そう、だがisobe-yaki.hateblo.jpで書いたようにニコ生ゲームから外部サイトへのアクセスはできないのでもちろんWebフォントも使えない。

otf埋め込み

多くのWebフォントはコンテンツへ埋め込んで使うこともできる*1ので、実はニコ生ゲームにフォントファイルを直接埋め込めばダイナミックフォントで使えるのだ。 フォントファイルの埋め込みやURLの取得方法については過去の記事参照。

isobe-yaki.hateblo.jp

まず、game.jsonに以下のように記述する。

{
  ...,
  "assets": {
    "font": {
      "type": "text",
      "path": "font/hogehoge.otf"
    }
  },
  ...
}

そのページで読み込んだフォントを使えるようにするためにはFontFace - Web APIs | MDNクラスを使う。詳しい説明は省くが、とにかくコンストラクタにフォントファイルのURLかバイナリデータを渡せばフォントをインストールできるようだ。

ここで少しハマったのだが、URLを渡すコンストラクタだとエラーが出てしまった。

new FontFace("_hoge", g.game._assetManager.configuration.font.path)
.load()
.then(font => {
    document.fonts.add(font);
});

エラー文は忘れたが、外部のアクセスはダメです的なやつだった気がする。なので最初は(フォント読み込み無理なのか?)と思ったが、バイナリデータを渡す方のコンストラクタで普通に読めた。なんでや

fetch(g.game._assetManager.configuration.font.path)
.then(b => b.blob())
.then(blob => blob.arrayBuffer())
.then(buf => {
    new FontFace("_hoge", buf)
    .load()
    .then(font => {
        document.fonts.add(font);
    });
});

ここまでできたら後はg.DynamicFontのコンストラクタでこのフォントの名前を指定すれば自由にフォントが使えるようになる。(もちろん非同期読込なので完了まで待ってからDynamicFontを作ること)

new g.DynamicFont({
    game: g.game,
    fontFamily: '_hoge',
    size: 30
});
フォントでかいよ・・・

とはいえニコ生ゲームは容量制限があって未圧縮状態で10MB以下でなければならない。フォントファイルは物によって全然サイズが違うが日本語フォントだと大体数MBは食うのでかなり厳しい。

やや力技だがフォントファイルを圧縮してからパッケージに含めて実行時に自前で展開するという方法もある。jsのライブラリでもzip展開できるのあるし検討してみるべきだろう。とは言えフォントファイルって70%くらいにしか縮まないので凝ったフォントはやはり厳しい。ちなみに自分はc++&emscriptenで開発しているので圧縮展開は自前のbzip系アルゴリズムを実装した。

追記

現在ゲームの容量制限が緩和されて30MBまで使えるようになっている。なので割と自由にフォントを使えそうだ。更に圧縮をする場合もCompression Streams API - Web API | MDNgzipが使えるので簡単だし展開速度も普通に速いので積極的に使っていいと思う。

*1:フォントごとにライセンスが異なるので使いたいフォントのライセンスをよく確認すること

任意のファイルを読み込めるようにする

AkashicEngineにはアセットをパッケージに含めるための仕組みとしてjsonフォーマットとコマンドラインツールが備わっている。

akashic-games.github.io

有効なファイルタイプとしてはscript,image,audio,textなどがある。エクスポートコマンドを実行するとgame.jsonに従って各アセットファイルが1つのzipにまとめられる。

この際、ファイルタイプによって次のような処理がされているっぽい。

  • scriptタイプのファイルは1ファイルにまとめられてかつminifyオプションが指定されていれば最小化もされる そのためjsとして解釈可能なテキストファイルでないとエラーとなる
  • imageタイプのファイルは専用のプロパティであるwidthやheightが正しいかどうかチェックされる そのためAkashicでサポートしているファイルフォーマットかつ、正しいサイズでないとエラーとなる
  • textタイプのファイルはエクスポート時にUTF-8エンコードされるのだがその条件はここにある通りだ
  • audioタイプのファイルは特に内容のチェックなどはされない
  • 全タイプ共通としてファイル名が謎のハッシュ値のような名前に置き換わる

これらを踏まえて、任意のファイルをパッケージに含めてそれを読み込む方法を考える。

game.json

まずファイルタイプであるが、scriptとimageに関してはエクスポート時に内容がチェックがされてしまい、適当なバイナリファイルなどだとエラーになるのでaudioかtextのどちらかを使うことになる。 よって1つの例として以下のようになる。

{
  ...,
  "assets": {
    "bin": {
      "type": "text",
      "path": "data.bin"
    }
  },
  ...
}

textタイプの場合前述の文字コード変換がかからないようにtextフォルダ以下に配置しない、拡張子としてtxt/jsonを避けるといった対策をする必要があることに注意。

これでエクスポートすることでdata.binというバイナリファイルをそのままパッケージ化できた。

読み込み

次に読み込み方法だが、ここはちょっとハック手法になる。通常アセットの読み込みにはScene | Akashic Engineクラスのコンストラクタに使いたいアセットのID配列を渡すか、アセットにglobalフラグを付けて読み込ませるが、これらの方法ではランタイムエラーが発生するので、ファイル読み込みはエンジンを介さず普通にfetch関数などブラウザで使えるデータ読み込み系のAPIを使う。以前の記事でも書いたが、パッケージに含まれるファイルというのは基本的にCDNサーバーに配置されており、ゲームから自由にアクセス可能なのだ。

APIに渡すアセットのURLをどうやって取得するか、結論から言うと以下のコードで取得できる。

g.game._assetManager.configuration[アセットID].path

アンダースコアが先頭についていることからもわかる通りエンジンのprivateメンバにアクセスしてしまっている(ここハックポイント)。そのため将来的にこの方法が使えなくなっても何も文句は言えない!

Base64

ちなみにだが、バイナリファイルをbase64エンコードしたものをtextタイプとして読み込んでゲーム実行時にデコードするという方法ももちろん可能である。base64エンコードをするとファイルサイズが1.33倍になるしデコード処理が要るし、何よりデータ作成した後いちいちbase64エンコードのプロセスが挟まってめんどいのでできればそのまま読めた方がいいよねということで上記のような読み込み方を採用した。


ここテストに出ます

というわけで、今回の記事はここまでですが今後の記事ではこのテク前提のテクも出てくることになるので、覚えておいてください✍

ニコ生ゲームに好きなDOM要素を乗っける

ニコ生ゲームは公式で紹介されている通り配信ページの映像部分にオーバーレイで表示される。DOMで言うと以下のような構造になってる。

<html>
<body>
    <!--視聴ページ-->
    <iframe>
        <!--ゲーム領域-->
        <div>
            <div>
                <!--タッチイベント用レイヤー-->
                <div>
                    <canvas><!--描画内容--></canvas>
                </div>
            </div>
        </div>
    </iframe>
</body>
</html>

で、ここに好きなDOM要素が追加できたら色々できそうではないか?というのが今回の内容の出発点。

例えばtextarea要素を置けば漢字や絵文字などをキーボードで入力できるようになるし、リンク要素を置けば外部リンクやセーブデータのDLリンクなんてのも作れる。

ゲームの上にテキストエリアとボカシ背景を追加した様子。ユーザーは任意の名前を入力できる

ではどのように要素を置けばこれらができるだろうか?普通に考えたらcanvas要素の兄弟(canvasは子を持てないので)として要素を追加したくなるがこれだと問題がある。上記疑似htmlを見ればわかるようにcanvasの親にタッチイベントハンドル用のdiv要素がある。こいつがあるとその向こう側の要素でタッチイベントが発火しないのでここではだめだ(タッチしなくていいなら問題ないけど)。ならそのdiv要素の兄弟にする?確かにそれならタッチイベントも受け取れるのでやりたいことはできる。しかし、ニコニコ側の都合でページの作りが変更されたら、変更の度にゲームを更新しなくてはいけない(めったにないと思うけど)。

そこでめんどいことを考えなくていいようiframe の最前面に要素を追加してしまう。つまりbody要素の末尾に追加すればいいだけだ。

<html>
<body>
    <!--視聴ページ-->
    <iframe>
        <!--ゲーム領域-->
        <div>
            <div>
                <!--タッチイベント用レイヤー-->
                <div>
                    <canvas><!--描画内容--></canvas>
                </div>
            </div>
        </div>
        <!--ここに好きな要素を追加しちゃう-->
    </iframe>
</body>
</html>

ただ追加してもcanvas要素の横や下に行って見えないので、canvasに重なるように、styleでpositionをabsoluteにし座標などを設定する。 canvasg.game.renderers[0].surface.canvasで取得できる。[0]としているところがやや不安要素だがまぁそうそう仕様が変わるところではないだろうと妥協する。 canvasのiframe内での絶対座標取得にはgetBoundingClientRect()を用いる。

// 新規div要素作成
const elem = document.createElement('div');
// 座標を絶対値に設定
elem.style.position = 'absolute';
// 最前面に追加
document.body.appendChild(elem);
// ゲームのキャンバスを取得
const cvs = g.game.renderers[0].surface.canvas;
// キャンバスの絶対位置を取得
const bounds = cvs.getBoundingClientRect();
// divに座標を設定
elem.style.left = bounds.left + 'px';
elem.style.top = bounds.top + 'px';
elem.style.width = bounds.width + 'px';
elem.style.height = bounds.height + 'px';

とりあえずこれで、キャンバスにDOM要素を重ねて表示することができるようになった。

でもこれだけだとちょっとまずくて、ウィンドウサイズが変わった時やフルスクリーンモードに切り替えたときなどにcanvasの位置やスケールが変わるとDOMがずれてしまう。そこで、以下のようにする。

// キャンバスのサイズ変更ハンドラ
const canvasLocation = new EventTarget();
const onWindowResize = () => canvasLocation.dispatchEvent(new Event('changed'));
// キャンバスのサイズとスケールが変化したら発火
new MutationObserver(onWindowResize).observe(cvs, {
    attributes: true,
    attributeFilter: ['width', 'height', 'style'],
});
// ウィンドウサイズが変化したら発火
window.addEventListener('resize', onWindowResize);

// サイズ変更時のハンドラを定義
const resized = () => {
    // キャンバスの絶対位置を取得
    const bounds = cvs.getBoundingClientRect();
    // divに座標を設定
    elem.style.left = bounds.left + 'px';
    elem.style.top = bounds.top + 'px';
    elem.style.width = bounds.width + 'px';
    elem.style.height = bounds.height + 'px';
};
// キャンバスサイズと位置変更を監視して常にキャンバスに合わせる
canvasLocation.addEventListener('changed', resized);

これでとりあえずやりたいことはできた。細かいことを2点付け足すとしたら、まずcanvas上の指定した位置とサイズに配置する方法だが、一応簡単に紹介する。

// サイズ変更時のハンドラを定義
const resized = () => {
    // キャンバスの絶対位置を取得
    const bounds = cvs.getBoundingClientRect();
    // キャンバスのスケールを算出
    const scalex = bounds.width / g.game.width;
    const scaley = bounds.height / g.game.height;
    // divに座標を設定
    elem.style.left = bounds.left + x * scalex + 'px';
    elem.style.top = bounds.top + y * scaley + 'px';
    elem.style.width = width * scalex + 'px';
    elem.style.height = height * scaley + 'px';
};

※これはゲームのアス比がキャンバスと同じ16:9である時の処理であることに注意

次にローカルでのデバッグ時の話なのだが、実は上記のコードだけでakashic serveコマンドのデバッグを行うと電源ボタンみたいなのを押してゲームをリセットしてもwindowへ登録したイベントリスナーは自動で解除されないのでリサイズイベントが二重三重に呼ばれるようになってしまう。そこで使えるのがSceneクラスのonStateChangeイベントで、'before-destroyed'が飛んで来たらwindowのイベントリスナを解除するコードを追加することで快適にデバッグもできるようになる。

scene.onStateChange.add(state => {
    if (state == 'before-destroyed') {
        // うまいことしてリロード前の後処理を入れとく
    }
});

(23/6/6追記)指摘があったので改めて確認したら電源ボタンでイベント飛んで来てなかった(あれ?)。windowオブジェクトに登録したイベントリスナーが二重三重で呼ばれてしまうようなので、無理くりリロードイベントを捕まえて解除するか、定期的にページリロードして蓄積したイベントリスナーを解放してください。

ニコ生ゲームの仕組み推察

この1年半くらいでニコ生ゲームの裏の仕組みが何となくつかめてきたので大体こうだろうという考察をまとめる。

 

まずランキング対応ゲーム。ゲーム自体はそれぞれ個人プレイで、制限時間終了時のスコアに応じてランキング画面が表示されるというもの。

  • ニコ生のWebページを生成しているのがWebサーバー
  • 動画を流しているのが配信サーバー
  • ゲームのセッションを管理しているのがセッションサーバー
  • ゲームアセットが置かれているのがCDN

とざっくり推察。生主がゲーム開始するとWebサーバーを介してセッションが立ち上がり、CDNに置かれているゲームデータのダウンロードが開始される。ユーザーはセッションサーバーと通信してスコアを送信、みたいな感じじゃなかろうか。

ちなみにニコ生ゲームはiframeの中で実行されている。このiframe内では通信先がセッションサーバーとCDN(と生ゲー用Webサーバー)に制限されていると思う。

よって全部試したわけではないが、以下のようなことはできないはず

  • ユーザーIDを元にユーザーのアイコン画像を取得
  • 外部サイトとの通信
  • 配信画面のキャプチャ

しかし、逆に言えばCDNはアクセスできるのでこれを利用したテクニックがいくつかある。今後の記事で紹介できたらと思う。

 

次にマルチ対応ゲーム。各ユーザーの操作が参加者全員に共有されゲームの状態も共通となり互いに干渉しながら進めるゲーム。

サーバー構成は大体ランキング対応ゲームと変わらないが、サーバー側でもゲームを動かすためnode.jsのサーバーが存在する(物理的にというわけではなく概念的に)。

nodeサーバーでは画像や音は不要なはずなのでCDNへのアクセス権が無い。そのためスクリプトだけがニコ生ゲーム登録時にnodeサーバーにコピーされていると思われる。

ちなみにニコ生にはタイムシフト視聴というものがある。あとから配信を見返すことができるのだが、この時ニコ生ゲームのプレイ内容も復元される。しかしもし開発者がそのゲームを更新してしまっていたら、新しいバージョンのゲームで以前のリプレイデータを再生されてしまうのだろうか?もちろんそんなことはなくタイムシフトが残っているゲームはその時の古いバージョンも残されていて、ちゃんと復元できるようになっている。では古いゲームはいつ削除されるのかというと恐らくそのバージョンを参照している全てのタイムシフトの期限が切れたタイミングではないかと思っている。まぁつまり開発者は細かいこと気にせずゲームを更新してしまっていいということだ。

 

あと、細かい話だがニコ生ゲームは更新したタイミングでの最新のAkashicEngineが使われるようになるっぽい。

どういうことかというと、以前パネワンを1年ぶりくらいに更新した時に、コードもデータもほぼ変えていないのにニコ生アプリ上で音が鳴らなくなるという現象が起きた。

どうやら、その数か月ほど前にAkashicEngineのサウンド周りに更新が入ったようで以前と同じデータでありながら実行時のAkashicエンジンのバージョンが上がったことによって不具合が起きるようになったのだ。*1

このことから、ゲーム更新時にCDNやnodeサーバーに一緒にAkashicEngine本体もコピーされているっぽいことが分かる。そんな非効率なことしないで常に最新のエンジン使えばいいじゃないかと思われるかもしれないが、そうすると上でも書いたタイムシフト時の復元性問題が出てくるのでそうもいかないことが分かる。

は最新のAkashicエンジンを常に取得する様子。いちいちコピーしなくてよいが放送時とタイムシフト時で異なるバージョンが読まれる可能性がある。

はゲーム更新時にCDNにAkashicをコピーする様子。少なくともこのゲームでは一生同じAkashicエンジンが使われるので動作が変わることはない。

実際にはコピーコストも考えてどのバージョンのエンジンを使うかバージョン情報だけ持たせてるとか工夫はしてそう。

まとめ
  • ユーザー環境からはエクスポートしたアセットに好きにアクセスできる
  • マルチのサーバー環境からはスクリプト以外のアセットは見れない

とりあえず今回はここだけ押さえておきましょう。

*1:※この件に関しては公式に問い合わせてニコ生アプリを修正してもらったのでもう大丈夫だと思う。

このブログについて

このブログはニコ生ゲーム開発に関する諸々のためのブログです。

 

元々開発の中で得た知見はたまにQiitaに書いていましたがニコ生ゲーム特化の話となるとあちらに書くわけにもいかないんで、もうちょい雑多に書く場所として開設しました。

なので、書くことなくなったら放置になると思いますが、やる気のあるうちに色々書いてみようと思います。

 

※当ブログは初心者向けではありません。初めてゲームを作る人や基本を学びたい人はこちらの方が参考になると思います。

nicorakku.hatenablog.com

逆に言うと↑に書いてるような基礎の話はうちでは一切書かないのでこちらと合わせて読むとより深まると思います。