Akashic Engine 新バージョンの機能紹介


はじめに

これは 第二のドワンゴ Advent Calendar 2020 21日目 の記事です。

僕はドワンゴでニコ生ゲーム等の作成が可能なゲームエンジンであるAkashic Engineの開発に携わっています。
本日は、Akashic Engineの新しいメジャーバージョンで追加される機能の一部について紹介していきたいと思います。

Akashic Engineと新しいメジャーバージョン

Akashic Engineとは, HTML上で動作するオープンソースのゲームエンジンで、JavaScriptやTypeScriptで記述することができます。これを用いてニコニコ生放送上で動作するニコ生ゲームを作成することができます。
Akashic Engineの最新メジャーバージョンはv2でしたが、現在新しいメジャーバージョンであるv3を開発中で近日公開する予定でいます。
メジャーバージョンアップといっても、 semver ライクにバージョン名をつけているので、v3.0.0 時点ですごく機能拡張があるわけではありません。しかし、いくつか個人的に便利だと思える機能がいくつかありますので、今回はそれらについて紹介していきたいと思います。

追加機能の紹介

アセットの一括指定

従来、あるシーンでアセットを利用する場合g.Sceneのコンストラクタ引数のassetIds?: string[]で利用するアセットIDを以下のように羅列するしかありませんでした。

var scene = new g.Scene({
  game: g.game,
  assetIds: ["img1", "img2", "img3", ...]
});

そこでv3では、assetPaths?: string[]でアセットのファイルパスを指定することも可能になりました。assetPaths ではglob のサブセット文法(**, *, ?) をサポートしています。 (** はあらゆるファイルや 0 個以上のディレクトリ、サブディレクトリにマッチします。 * は 0 文字以上任意の文字列にマッチします。 ? は任意の 1 文字にマッチします。)
これによって、あるシーンで利用する画像アセットをimage/scene1にまとめて配置している場合は以下のように指定することによってアセットIDを羅列することなく簡潔に利用するアセットを指定することができます。

var scene = new g.Scene({
  game: g.game,
  assetPaths: ["/image/scene1/**/*"]
});

ただし、assetPathsで指定するファイルパスはgame.json のあるディレクトリをルート(/)とする絶対パスで表記する必要があります。それに加えて、指定できるアセットもassetIdsで指定する時と同様に事前にgame.jsonに登録しておく必要があります。

また、以下のように指定することによってgame.jsonに登録したアセットを全て読み込むことができます。シーンが1つしかないコンテンツ等はこのような指定方法が利用できるのではないかと思います。

var scene = new g.Scene({
  game: g.game,
  assetPaths: ["/**/*"]
});

アセット配置用のディレクトリとして assets/ を追加

これは厳密にはAkashic Engineというよりakashic-cliのお話なのですが、以下のようなコマンドをgame.jsonが配置されているディレクトリで実行することによって対象ディレクトリに配置しているアセットがgame.jsonに登録されます(このコマンドに関する詳細はこちらを参照してください)。

akashic scan asset

しかし従来の仕様ではアセットを配置する対象ディレクトリが限定されていたり(例:画像アセットは image/ ディレクトリに配置)、ファイル名がそのままアセットIDになるといった制約があり、それによって以下のような問題点がありました。

  • ファイル名が被るとエラーになる (chara1.png と chara1.json みたいな名前で置けない)
  • 役割(シーン)別にファイルをまとめられない (image/stage1_map.png と text/stage1_mapData.json のように分けないといけなかった)

この問題点を解決するために、アセット配置用のディレクトリとして assets/ を追加して、assets/ には様々なアセットを配置できるようにしました。それによって以下のようなことが可能になりました。

  • assets/stage1/map.png, assets/stage1/mapData.json のように違う種類のアセットをシーン毎にまとめて同じディレクトリに配置可能
  • assets/stage1/map.png, assets/stage2/map.png のように同じファイル名のアセットを配置可能

ただし、assets/ 以下はアセットIDが不定になるので、アセットの読み込みは assetPaths でしかできないことに注意してください。

g.Scene#asset を追加

新しく追加されたg.Scene#assetのメソッドを利用することでアセットをIDやファイルパス等で種類別に取得することができるようになりました(名前的に紛らわしいのですが、これはg.Scene#assetsとは別物です。g.Scene#assetsはアセットのマップです)。取得方法とメソッドは以下の通りです。

  • ファイルパス形式での取得: getImage(), getAudio() など
  • ファイルパスのパターンから複数まとめて取得: getAllImages(), getAllAudios() など
  • アセット ID からの取得: getImageById(), getAudioById() など

これによってたとえば次のように、ファイルパス形式でアセットを種類ごとに取得できます。

// 画像アセットの取得
var playerImage = scene.asset.getImage("/assets/player/image.png");

// オーディオアセットの取得
// (game.json での記述同様、オーディオアセットに限り、拡張子抜きで指定する必要があります)
var bgm = scene.asset.getAudio("/audio/bgm01");
bgm.play();

getAllImages()等のファイルパターンから複数取得するメソッドは該当するアセットの配列を返します。 アセットの指定には assetPaths と同様に glob のサブセット文法(**, *, ?) を利用できます。

// 複数の画像アセットの取得
var thumbnails = scene.asset.getAllImages("/assets/**/*.png");
for (var i = 0; i < thumbnails.length; i++) {
  var thumbnail = new g.Sprite({
    scene: scene,
    src: thumbnails[i],
    width: thumbnails[i].width,
    height: thumbnails[i].height
  });
  ...
}

アセット ID による取得 (getImageById() など) は、既存の g.Scene#assets と同様の機能です。 ただし TypeScript では as g.ImageAsset 等のダウンキャストが不要になります。

// g.Scene#assets でのアセットの取得
const imageWidth = (scene.assets["foo"] as g.ImageAsset).width;

// getImageById()での取得
const imageWidth = scene.asset.getImageById("foo").width;
// (返り値の型が ImageAsset になっているので、キャストなしで `width` にアクセスできます)

ニコ生ゲームでユーザー名の取得

これはv3.0.0以降のお話になってしまうのですが、@akashic-extension/resolve-player-infoというニコ生ゲームでユーザー名の取得をするための拡張ライブラリを開発中です。
今までのニコ生ゲームでは、各プレイヤーのユーザーIDしか取得できず、プレイヤー名を「プレイヤー12345」のような名前にするかコンテンツ毎に名前を入力するUIを独自で設ける等する必要があったかと思います。
しかし、このライブラリをコンテンツで利用することによって「ニコニコ生放送のユーザ名」を取得・利用することができるようになります。対応バージョンは Akashic Engine v3 以降です。

ただし自作ニコ生ゲームにおいては、ユーザの許可なくユーザ名を利用することはできません。コンテンツからユーザ名を取得しようとすると、ゲーム画面に許諾を求めるダイアログが表示されます。(下図参照)

ゲームからは、ここで「ユーザー名」ボタンを押したユーザの名前だけが取得できます。許諾されなかった場合、ランダムに生成された名前 (e.g. "ゲスト383") が通知されます。
このような制約を設けるのは、ニコニコ生放送が歴史的に「名前を出さない」コミュニケーションを許容してきたという文化的背景があるためです (184 コメント など)。

game.json の defaultLoadingScene"compact" を追加

game.jsonの defaultLoadingScene"compact" を指定した時、ローディング画面が以下のように表示されます。

  • 背景が透過になります。 (上記画像では灰色部分が背景で透過されて表示されています。)
  • プログレスバーが画面中央ではなく右下の方に小さく表示されます。

g.Game#localRandom を追加

マルチプレイの各プレイヤー間で共有されない (異なるシードを持つ) 乱数生成器 g.Game#localRandom が追加されます。
既存のg.Game#randomはプレイヤー全員に対して同じ乱数を生成するのに対して、g.Game#localRandomは各プレイヤー固有の乱数を生成します。
ただし、g.Game#localRandomはローカル処理 (local: true を指定したエンティティのイベントハンドラ (onPointDown など) またはそこから呼び出された処理) の中でのみ利用してください。それ以外で利用した場合、グローバルな状態が破壊されてしまう可能性があります。

// OKな例
var localSprite = new g.Sprite({
  scene: scene,
  src: scene.asset.getImage("image"),
  width: 100,
  height: 100,
  local: true // これを指定するとローカルエンティティ(1プレイヤーにしか見えないor影響のないエンティティ)になる
});
localSprite.update.add(() => {
  // localSpriteの大きさがランダムで変わる処理を以下に記述
  var rand = g.game.localRandom.generate(); // 0以上1未満の整数を取得。この値は各プレイヤーで異なる
  // このエンティティは1プレイヤーにしか見えないローカルなものなのでどのように大きさを変えてもグローバルな状態に影響ないので問題ない
  localSprite.width = 100 * rand;
  localSprite.height = 100 * rand;
  localSprite.modified();
});


// この使い方はNG
var sprite = new g.Sprite({
  scene: scene,
  src: scene.asset.getImage("image"),
  width: 100,
  height: 100
});
sprite.update.add(() => {
  var rand = g.game.localRandom.generate();
  // このspriteは全プレイヤーに見えているグローバルなものなのでプレイヤー毎に異なる大きさにしてしまうとグローバルな状態が破壊されることになる
  sprite.width = 100 * rand;
  sprite.height = 100 * rand;
  sprite.modified();
});

おわりに

ここまでAkashic Engineの最新メジャーバージョンであるv3で追加される新機能についてお話してきました。
これは近日正式公開致しますので、もう少しだけお待ちいただければと思います。
また、v3公開後でも現行のニコ生ゲームはそのまま動作しますので、ニコ生ゲーム開発者の方はご安心いただければと思います。

参考資料