Skip to content

端末に設定を保持する

マルチプレイニコ生ゲームの終了について

マルチプレイニコ生ゲーム (ランキング形式以外のゲーム) は、2026 年 2 月下旬をもって提供を終了させていただくことになりました。 このページでは一部マルチプレイ特有の記述が残っていますがご容赦ください。 なお文中のサンプルコードなどはランキングゲームでもご利用いただけます。

関連: 【重要/ニコニコ生放送】2026年2月より「ニコ生ゲーム/放送ネタ」の運用体制とサービス内容を変更します

ゲームによっては、プレイヤーが選択できる設定項目があります。

  • BGM の有無の切り替え
  • ゲームの難易度選択
  • 細かなルールのカスタマイズ など

ゲームが繰り返し遊ばれる場合、この種の設定は「最後に選ばれた値」が保存されていると便利です。 拡張ライブラリ @akashic-extension/instance-storage (インスタンスストレージ) を使うと、「端末やブラウザに設定項目の値を保存しておく」ことができます。

対応バージョンは Akashic Engine v3 以降です。 akashic sandbox, akashic serve でこの機能の動作を確認するには、 akashic-cli@3.0.0 以降が必要です。

TIP: LocalStorage との関係

一般的な Web 環境では、このような「値を端末・ブラウザに記憶させておく」機能として Web Storage API (localStorage) が利用できます (参考: MDN の『ウェブストレージ API の使用』(外部サイト)) 。 しかし自作ニコ生ゲームでは、セキュリティ上の制限のため localStorage を直接には利用できません。

@akashic-extension/instance-storage は、この代替となる機能を提供するライブラリです。

ただし技術的制約から、インスタンスストレージの API は localStorage と互換ではありません (非同期型の API になっており、やや煩雑です) 。 一方で、ゲームごとに保存領域が分離されているため、誤って他のゲームのデータを参照や上書きする恐れがないというメリットもあります。

ライブラリの導入

端末・ブラウザに値を保存するには、拡張ライブラリ @akashic-extension/instance-storage が必要です。

game.json のあるディレクトリで、次を実行してください。

sh
akashic install @akashic-extension/instance-storage

テキストエディタで game.json を開いて、次のような environment.external.instanceStorageLimited プロパティがなければ作成してください。値は "0" としてください。(v2.2.4 以降の akashic-cli では、 akashic install 時に自動的に作成されます。)

json
{
  ...,
  "environment": {
    "external": {
      "instanceStorageLimited": "0"
    }
  }
}

スクリプトアセット内で、 importrequire() を使って instanceStorage を取得します。

JavaScript
const { instanceStorage } = require("@akashic-extension/instance-storage");
TypeScript
import { instanceStorage } from "@akashic-extension/instance-storage";

この instanceStorage を使って値の保存と読み込みを行います。

インスタンスストレージと非同期 API

インスタンスストレージは主に次の機能を提供します。

  • キー名に紐づけた値の保存
  • キー名に紐づけられた値の読み込み
  • 保存された値の削除

ただしすべて非同期 API になっています。つまり呼び出した時点では処理が完了しておらず、別途完了を待つ必要があります。 たとえば値を読み込む instanceStorage.read() は、戻り値として「読み込まれた値」ではなく Promise を返します。

TIP: Promise

Promise は、JavaScript において非同期処理 (の結果) を表すオブジェクトです。 Promiseawait することで、処理の完了を待ち、その結果を得ることができます。

処理をフレーム単位で行う (フレームに同期して処理を進める) ゲームプログラムでは、非同期処理はあまり登場しません。 ここでは最低限インスタンスストレージを扱うために必要な部分のみ解説します。

一般的な Promise の詳細については、MDN の 『プロミスの使用』(外部サイト) などを参照してください。

呼び出し後、処理が完了するまでに何フレームかかるかは不定です。 そもそも値が保存されているかどうかや、保存されている値自体も端末・ブラウザに依存します。

そのため、インスタンスストレージへのアクセスは ローカル処理 として扱う必要があります。

TIP: ローカル処理

Akashic Engine のマルチプレイは原則「全員が同じ経過フレーム数で同じ入力を処理すれば、同じ状態に到達しているはずだ」という仮定に基づいています。 (参考: Akashic Engine 入門 » マルチプレイの基礎 » Akashic におけるマルチプレイ)

この仮定に従わないもの、すなわち「プレイヤーごとに異なるもの」を Akashic では「ローカルである」と呼びます。 たとえばローカルエンティティ (プレイヤーごとに表示が異なるエンティティ) やローカルイベント (プレイヤーごとに発生の有無や内容が異なるイベント) があります。 ローカルな処理は、ローカルでない状態 (他プレイヤーの動作に影響する状態) を変更してはいけません。 (参考: Akashic Engine 入門 » マルチプレイの基礎 » プレイヤーごとに異なる描画を行う)

なおランキング形式のニコ生ゲームの場合は、この「ローカルか否か」を気にする必要はありません。 ランキングゲームは「一人プレイのゲームを各人がプレイして、最終的なスコアを集計してランキングにする」形式で、 ゲーム内で他プレイヤーと同期する構造ではないためです。

また非同期 API ではしばしば、非同期処理が完了するまでゲームの進行を止める必要があります。 そのため非同期 API を扱うコードは煩雑になりがちです。

この複雑さを避けて、よくある「設定の保存」を実現するには、次のような形が簡単です。

  1. 値の読み込みは、最初のシーンのロード中にまとめて行う
  2. 読み込んだ値は変数に保持する。以降はこの変数だけを参照する
  3. 値を保存・削除する場合は、(2) の変数も同じように変更する
  4. 保存・削除時には非同期待ちしない

つまり ゲーム開始時一括読み込み する形です。 これにより非同期待ちは最初の値の読み込み時のみにでき、コードが単純になります。 以下のサンプルコードもこの指針に従います。

TIP

これはあくまで簡単のための指針です。 必要なら特にこれに囚われず、ローカル処理の制約 (非ローカルなゲームの状態を破壊しない) の範囲で任意に扱ってください。

保存された値を読み込む

保存された値の読み込みは、前述のとおり instanceStorage.read() で行います。

js
(async () => {
  const value = await instanceStorage.read(key);
})();

ここで key は読み込みたい値に紐づけたキー名 (文字列) です。

値が保存されていない場合、戻り値は null (で解決される Promise) になります。

TIP

前述のとおり await に注意してください。

await するとその後のコードの実行は非同期処理の完了まで遅延されます。 さらに、完了のタイミングがプレイヤーごとに異なりうるため、 関数内で await の後に書かれている処理はローカル処理になります 。 また await を使う関数は async 関数として定義する必要があります。

シーンロード中の読み込み

上記「ゲーム開始時一括読み込み」方式の場合、値の読み込みは最初のシーンのローディングに合わせて行います。

TIP

シーン (で利用するアセット) のロードはもともと非同期処理で、 Akashic Engine はロードを待つ間ローディングシーンを表示するようになっています。 この仕組みに便乗して、ローディングシーンの終わり際にストレージアクセスを済ませてしまうことで、 Promise とローカル処理の煩雑さを避けることができます。

これにはいくつかの方法があります。ここでは g.game.pushScene()prepare オプションを利用します。 prepare は「シーンのロード完了後、onLoad の通知前」に任意の非同期処理を実行する機能です。

js
g.game.pushScene(scene, {
  prepare: (done) => {
    ...
    done(); // 処理を終えて `onLoad` 通知に進む際に呼び出す
  }
});

parepare オプションには関数を指定します。この関数は、呼び出しの際に引数として関数 (done) が与えられます。 処理を完了して onLoad 通知に進みたい時に done() を呼び出してください。

これを使って、インスタンスストレージから設定を読み込む処理は次のように書けます。

js
// 読み込んだ設定を保持するオブジェクト
g.game.vars.config = {};

// 通常どおりのシーンの作成
const scene = new g.Scene({ game: g.game, assetPaths: [] });

// 通常どおりのロード後の処理
scene.onLoad.add(() => {
  // ...

  // この時点で読み込まれた設定を参照可能
  // 設定はこの値を通してしかアクセスしない
  if (g.game.vars.config.mute) {
    // ミュートする設定だった場合は BGM 音量を 0 に
    g.game.audio.music.volume = 0;
  }
});

g.game.pushScene(scene, {
  // アセットロード完了後、 onLoad 通知の前に実行したい関数を指定
  prepare: async (done) => {
    // 音声をミュートするかの設定を読み込み (なければ false)
    g.game.vars.config.mute = (await instanceStorage.read("mute")) ?? false;
    // 1 ゲームのプレイ時間の設定を読み込み (なければ 3 分)
    g.game.vars.config.time = (await instanceStorage.read("time")) ?? 3;

    done(); // 呼び出しでローディングシーンを取り除き、onLoad 通知へ
  }
});
ts
// ゲームの設定項目の値を表す型
// (ただしこのサンプルコードで設定項目を保持する `g.game.vars` は `any` 型なので、
// 実際には型なしで何でも代入できます。ここでは分かりやすさのため型をつけています)
interface Config {
  // BGM をミュートするか否か
  mute: boolean;
  // プレイ時間 (単位は分)
  time: number;
}

// 読み込んだ設定を保持するオブジェクト
g.game.vars.config = {
  mute: false,
  time: 3,
} as Config;

// 通常どおりのシーンの作成
const scene = new g.Scene({ game: g.game, assetPaths: [] });

// 通常どおりのロード後の処理
scene.onLoad.add(() => {
  // ...

  // この時点で読み込まれた設定を参照可能。
  // 設定はこの値を通してしかアクセスしない
  const config: Config = g.game.vars.config;
  if (config.mute) {
    // ミュートする設定だった場合は BGM 音量を 0 に
    g.game.audio.music.volume = 0;
  }
});

g.game.pushScene(scene, {
  // アセットロード完了後、 onLoad 通知の前に実行したい関数を指定
  prepare: async (done) => {
    const config: Config = g.game.vars.config;
    // 音声をミュートするかの設定を読み込み (なければ false)
    config.mute = (await instanceStorage.read("mute")) ?? false;
    // 1 ゲームのプレイ時間設定を読み込み (なければ 3 分)
    config.time = (await instanceStorage.read("time")) ?? 3;

    done(); // 呼び出しでローディングシーンを取り除き、onLoad 通知へ
  }
});

値を保存・削除する

値の保存は instanceStorage.write(), 削除は instanceStorage.delete() で行います。

js
instanceStorage.write(key, value); // 保存
instanceStorage.delete(key); // 削除

どちらも key は値に紐づけるキー名 (文字列) です。 write() には、保存したい値 value を併せて指定してください。 value は JSON として妥当な値である必要があります。

どちらも非同期処理で、戻り値は Promise (処理完了時に解決される) です。

TIP

通常この Promise の完了を待つ必要はありません。 この Promiseawait して得られる値は undefined のみです。 read() は先行する write(), delete() の完了を自動的に待ちます。

変数とセットでの保存・削除

上記「ゲーム開始時一括読み込み」方式の場合、保存・削除は「読み込み時に代入した変数の更新」とセットで行います。

先ほどのコード例では、次のように「BGM のミュート設定を切り替えるボタン」を作ることができます。

js
// ミュート切り替えボタン
const muteToggleButton = new g.Sprite({
  src: ..., // 適当なボタン画像
  ...,
  touchable: true, // クリック・タップできるようにする
  local: true, // ローカルエンティティにする (重要)
});

// ボタンが押された時の処理
muteToggleButton.onPointDown.add(() => {
  // 次の状態を求決める
  const muteNext = !g.game.vars.config.mute;

  // 設定を保持する変数 (g.game.vars) を更新しつつ、保存を実行
  g.game.vars.config.mute = muteNext;
  instanceStorage.write("mute", muteNext);

  // 設定変更を反映
  // 設定自体は常に g.game.vars から参照する (再度の read() はしない)
  if (g.game.vars.config.mute) {
    g.game.audio.music.volume = 0;
  } else {
    g.game.audio.music.volume = 1;
  }
});

変数とセットで更新することで、新しい値を参照する時に instanceStorage.read() をし直さずに済みます。

INFO

なおこの方式では、厳密な「最後に保存された値」は取得できません。 たとえば「複数タブで同じゲームを複数同時にプレイしていて、別のタブから値が保存された時」は、 変数に保持している値と instanceStorege.read() の結果が食い違います。 ただしこれが問題になることは稀でしょう。

TIP: local: true の指定に注意

上のコードで、 g.Sprite の生成時に local: true を指定している (ローカルエンティティ にしている) 点に注意してください。 ミュート設定はあくまでローカルな情報 (他プレイヤーに影響がない) で、切り替えもローカルに行いたいためです。

このコード例で muteToggleButton をローカルエンティティにしなかった場合、マルチプレイの誰かがこのボタンをクリックする度、 全プレイヤーのミュート設定が切り替わることになってしまいます。

TIP: プレイ時間設定はローカルな情報か

先ほどのコード例の g.game.vars.config.time 「1 ゲームのプレイ時間の設定」は、 (BGM のミュート設定と異なり) 意味的にはローカルな情報ではない (全プレイヤー共通でないとおかしい) ので、例として紛らわしいかもしれません。

ここでは、「ゲーム開始時に放送者がルール設定を行う」形式のゲームを想定しています。

  • g.game.vars.config.time は、放送者にだけ表示される (ローカルエンティティの) 設定画面でだけ参照・変更する
  • 放送者が最終的に選んだルール設定は、g.game.raiseEvent() で全プレイヤーに通知する
  • (放送者も含め) 全プレイヤーは、raiseEvent() で通知された設定情報に基づいてメインゲームを開始する

この形であれば、「1 ゲームのプレイ時間の設定」自体はあくまでもローカルな情報です。

サンプルコード

上記のBGM ミュート設定のボタンをまとめると、次のようなコードになります。

js
import { instanceStorage } from "@akashic-extension/instance-storage";

// 読み込んだ設定を保持するオブジェクト
g.game.vars.config = {
  mute: false,
};

function main(param) {
  const scene = new g.Scene({
    game: g.game,
    assetPaths: [
      "/image/button_muted.png", // ミュート状態を表すボタン画像
      "/image/button_unmuted.png", // 非ミュート状態を表すボタン画像
      "/audio/bgm" // BGM
    ]
  });
  scene.onLoad.add(() => {
    // アセットオブジェクトを取得
    const bgmAsset = scene.asset.getAudio("/audio/bgm");
    const buttonImageMuted = scene.asset.getImage("/image/button_muted.png");
    const buttonImageUnmuted = scene.asset.getImage("/image/button_unmuted.png");

    // ミュートを切り替えるボタンを生成
    const muteToggleButton = new g.Sprite({
      scene: scene,
      src: buttonImageUnmuted,
      x: (g.game.width - buttonImageUnmuted.width) / 2,
      y: (g.game.height - buttonImageUnmuted.height) / 2,
      local: true, // ローカル (プレイヤーごとに異なる動作をする) を指定
      touchable: true,
    });

    // ミュート切り替え。引数の指定に応じて BGM の音量を設定し、mutoToggleButton の画像を切り替える。
    function setMuted(toMute) {
      if (toMute) {
        g.game.audio.music.volume = 0;
        muteToggleButton.src = buttonImageMuted;
        muteToggleButton.invalidate();
      } else {
        g.game.audio.music.volume = 1;
        muteToggleButton.src = buttonImageUnmuted;
        muteToggleButton.invalidate();
      }
    }

    // ミュート切り替えボタンが押された時
    muteToggleButton.onPointDown.add(() => {
      // 現在の値を参照して次の状態を決める
      const muteNext = !g.game.vars.config.mute;

      // 設定を保持する変数 (g.game.vars) を更新しつつ、保存を実行
      g.game.vars.config.mute = muteNext;
      instanceStorage.write("mute", muteNext);

      // 設定変更を反映 (設定は g.game.vars から参照し、read() はしない)
      setMuted(g.game.vars.config.mute);
    });

    // 初期状態として g.game.vars.config に読み込まれた設定を反映
    if (g.game.vars.config.mute)
      setMuted(true);

    // BGM 自体は再生しっぱなしにする
    bgmAsset.play();

    scene.append(muteToggleButton);
  });

  // シーン遷移。遷移のローディングに合わせて prepare で設定読み込みを行う
  g.game.pushScene(scene, {
    prepare: async (done) => {
      const config = g.game.vars.config;
      config.mute = (await instanceStorage.read("mute")) ?? false;
      done();
    }
  });
}

export = main;
ts
import { instanceStorage } from "@akashic-extension/instance-storage";

// ゲームの設定項目の値を表す型
// (ただしこのサンプルコードで設定項目を保持する `g.game.vars` は `any` 型なので、
// 実際には型なしで何でも代入できます。ここでは分かりやすさのため型をつけています)
interface Config {
  mute: boolean;
}

// 読み込んだ設定を保持するオブジェクト
g.game.vars.config = {
  mute: false,
} as Config;

function main(param: g.GameMainParameterObject): void {
  const scene = new g.Scene({
    game: g.game,
    assetPaths: [
      "/image/button_muted.png", // ミュート状態を表すボタン画像
      "/image/button_unmuted.png", // 非ミュート状態を表すボタン画像
      "/audio/bgm" // BGM
    ]
  });
  scene.onLoad.add(() => {
    // アセットオブジェクトを取得
    const bgmAsset = scene.asset.getAudio("/audio/bgm");
    const buttonImageMuted = scene.asset.getImage("/image/button_muted.png");
    const buttonImageUnmuted = scene.asset.getImage("/image/button_unmuted.png");

    // ミュートを切り替えるボタンを生成
    const muteToggleButton = new g.Sprite({
      scene: scene,
      src: buttonImageUnmuted,
      x: (g.game.width - buttonImageUnmuted.width) / 2,
      y: (g.game.height - buttonImageUnmuted.height) / 2,
      local: true, // ローカル (プレイヤーごとに異なる動作をする) を指定
      touchable: true,
    });

    // ミュート切り替え。引数の指定に応じて BGM の音量を設定し、mutoToggleButton の画像を切り替える。
    function setMuted(toMute: boolean) {
      if (toMute) {
        g.game.audio.music.volume = 0;
        muteToggleButton.src = buttonImageMuted;
        muteToggleButton.invalidate();
      } else {
        g.game.audio.music.volume = 1;
        muteToggleButton.src = buttonImageUnmuted;
        muteToggleButton.invalidate();
      }
    }

    // ミュート切り替えボタンが押された時
    muteToggleButton.onPointDown.add(() => {
      // 現在の値を参照して次の状態を決める
      const muteNext = !g.game.vars.config.mute;

      // 設定を保持する変数 (g.game.vars) を更新しつつ、保存を実行
      g.game.vars.config.mute = muteNext;
      instanceStorage.write("mute", muteNext);

      // 設定変更を反映 (設定は g.game.vars から参照し、read() はしない)
      setMuted(g.game.vars.config.mute);
    });

    // 初期状態として g.game.vars.config に読み込まれた設定を反映
    if (g.game.vars.config.mute)
      setMuted(true);

    // BGM 自体は再生しっぱなしにする
    bgmAsset.play();

    scene.append(muteToggleButton);
  });

  // シーン遷移。遷移のローディングに合わせて prepare で設定読み込みを行う
  g.game.pushScene(scene, {
    prepare: async (done) => {
      const config = g.game.vars.config as Config;
      config.mute = (await instanceStorage.read("mute") as boolean | null) ?? false;
      done();
    }
  });
}

export = main;

利用上の注意

インスタンスストレージの利用には、いくつか注意点があります。

ローカルな情報として扱う

上述のとおり、インスタンスストレージに保存されている値は端末・ブラウザ依存で、読み込みにかかる時間も一定ではありません。 同じプレイヤーでさえ、PC とタブレットで異なる値が保存されていることはごく自然にあり得ます。

従って インスタンスストレージから得た値は常にローカルな情報として扱う 必要があります。 すなわち、インスタンスストレージの値で非ローカルなゲームの状態を変更してはいけません。

たとえば早い者勝ちの宝探しゲームで、宝のありかの座標をインスタンスストレージの値に応じて変えてしまうと、 「プレイヤー A の端末ではプレイヤー B がお宝を発見しているのに、プレイヤー C の端末では B は発見できなかった」という風に、 マルチプレイの整合性が破綻してしまいます。

特定のプレイヤーが保持していた値 (放送者の設定など) を全員が利用する場合は、(そのプレイヤーだけ) g.game.raiseEvent() を行なって、 全プレイヤーにイベントとしてその値を通知してください。 (e.g. 「宝のありか」はインスタンスストレージの値ではなく、イベントで通知された値で決める)

なくてもよい・消えてもよい値にのみ使う

インスタンスストレージの情報は、任意に消される場合があります。

これは環境によっては内部的に localStorage を使っており、ユーザのキャッシュクリアなどで消されてしまう場合があるためです。 また他ゲームとの容量の兼ね合いで、あまり利用されていないデータは削除されることがあります。

そのため 常に「値がない」ケースを想定してコードを書く 必要があります。 また、消えると致命的な情報 (セーブデータなど) を保持することはできません。 あくまで「あると便利な」設定などを保持する機能として扱ってください。

容量制限

1 つの自作ニコ生ゲームが保存できるデータの 容量には上限があります 。 現在のところ、この上限は (文字列化した状態で) 1KB です。 それ以上のデータは書き込みに失敗するか、または後から削除される可能性があります。

特に (実態として localStorage を利用する環境では) 一つの localStorage を全ゲームで使うことになるため、localStorage と比較するとかなり厳しい制限を課しています。

回数制限

1 回のゲームプレイ中、インスタンスストレージの API 呼び出し回数には制限があります 。 現在のところ、その回数は 512 回です。 これを超過した後の read() はすべて null を返し、 write()delete() は何もしません。

これは「誤って無限に書き込みを行ってしまい、端末・ストレージに過剰な負荷がかかる」といったトラブルを回避するための措置です。

TIP

この観点からも、上述の「ゲーム開始時一括読み込み」形式 (read() を最低限しか行わず、 write() は変数とセットで更新して、変数だけを参照する) をおすすめします。

Android 非対応 (2025 年 11 月時点)

実装上のトラブルのため、現在のところこの機能は Android アプリではサポートされていません。 read() はすべて null を返し、 write()delete() は何もしません。

TIP

どちらにせよ、インスタンスストレージは常に「値がない」ケースを想定してコードを書くことになるため、 ニコ生ゲームのプログラム上で Android を考慮した特別な処理は必要ありません。 ただし使い方によっては、ユーザへの案内上言及いただく必要があるかもしれません。

また Android の不具合修正後、ライブラリの更新などニコ生ゲーム側の対応は必要ない見込みです。

仕様

以下は "@akashic-extension/instance-storage" から require/import できる値 instanceStorage のメソッドとその仕様です。

TIP

その他の機能は API リファレンス を参照してください。

read()

ts
instanceStorage.read(key);

インスタンスストレージから値を読み込みます。

引数省略可内容
key文字列不可値に紐づけたキー名

戻り値は、取得した値で解決される Promise です。 値がない場合、Promisenull で解決されます。

write()

ts
instanceStorage.write(key, val);

インスタンスストレージに値を書き込みます。

引数省略可内容
key文字列不可値に紐づけるキー名
val任意不可保存する値。JSON として妥当な値でなければならない

戻り値は、完了時に解決される Promise です。

delete()

インスタンスストレージから値を削除します。

ts
instanceStorage.delete(key: string): Promise<void>;
引数省略可内容
key文字列不可値に紐づけたキー名

戻り値は、完了時に解決される Promise です。

getLength()

ts
instanceStorage.getLength(): Promise<number>;

インスタンスストレージに保存されている値の要素数を取得します。

戻り値は、要素数に解決される Promise です。

getKey()

ts
instanceStorage.getKey(index: number): Promise<string | null>;

インスタンスストレージのインデックス番号からキーを取得します。

引数省略可内容
index数値不可キー名を得たいインデックス。
0 以上、getLength() の返した値未満の整数

戻り値は、キー名で解決される Promise です。 対応するキーがない場合、Promisenull に解決されます。