この記事について

2018年10月25日より、実験放送で利用できるゲームが、RPGアツマールに投稿できるようになりました。

現在は自作ゲームフェスの中で、実験放送ゲーム部門というコンテストも実施しています。

本記事では、2018年11月28日現在、ニコニコ新市場対応コンテンツでマルチプレーのゲームを作るための情報を、提供できる範囲で提供していきます。

期間的にコンテストには間に合わないかもしれませんが、本記事が今後、より興味深いコンテンツ制作への道しるべとなれば幸いです。

ユーザIDの取得

まず、マルチプレーでは、コンテンツ側でユーザIDを取得したいケースがあります。 本項ではAkashic Engineにおけるユーザ情報の取得方法について解説します。

手段は大きく2つあります。

  • g.Game#selfId
  • 各Triggerのe#player#id

以下のコードで、実行しているユーザIDを取得できます。

g.game.selfId

以下のコードで、そのイベントを実行したユーザのユーザIDを取得できます。

hoge.pointDown.add(function(e) {
    if (e.player && e.player.id) {
    }
});

以下のコードで、そのイベントを実行したユーザが、実行している端末のユーザかを判別できます。

hoge.pointDown.add(function(e) {
    if (e.player && e.player.id === g.game.selfId) {
    }
});

マルチプレーモード

動作確認の方法が乏しいため、これまで情報を出していませんでしたが、実はマルチプレーのゲーム・ツールも作成できます。本項では、コンテンツをマルチプレーに対応させるための方法を解説します。

以下に、コンテンツ制作講座第2回で利用したハートを出すツールを、このモードに対応させたバージョンのコードが置いてあります。

マルチプレーモードで動作させるには、game.jsonのsupportedModeに「multi」という指定を加えるだけです。ハートを出すツールのgame.json全体としては以下のようになります。

{
    "width": 640,
    "height": 480,
    "fps": 30,
    "main": "./script/main.js",
    "assets": {
        "main": {
            "type": "script",
            "path": "script/main.js",
            "global": true
        },
        "heart": {
            "type": "image",
            "width": 64,
            "height": 64,
            "path": "image/heart.png"
        }
    },
    "environment": {
        "sandbox-runtime": "2",
        "niconico": {
            "supportedModes": [
                "single",
                "multi"
            ]
        }
    }
}

これで、これまでは放送者だけがハートを出すツールだったのを、視聴者もワンタップでハートを出せるようになりました。

ローカル処理

特にマルチプレーモードにおいて、「そのユーザの手元にだけ表示するUI」というのを作成したくなるはずです。 本項では、その具体的な方法について解説します。

そのユーザの手元にだけ表示するUIは、Akashicの機能である「local」という機能を活用することができます。

ハートを出すツールを、実際に改造してみたいと思います。 今回はUIを出すので少しコードが長くなってしまいますが、以下が全コードになります。

function main(param) {
    var scene = new g.Scene({game: g.game, assetIds: ["heart", "spade", "sparkle"]});
    var color = "heart";
    scene.loaded.add(function() {
        function generateHeart(x, y, color) {
            // heart変数にheart画像のSpriteを追加
            var heart = new g.Sprite({
                scene: scene,
                src: scene.assets[color],
                parent: container,
                x: x,
                y: y,
                tag: {
                    counter: 0
                }
            });
            // 毎フレーム実行されるイベントであるupdateにイベントを登録
            heart.update.add(function(e) {
                // 毎フレームカウンタを追加
                heart.tag.counter++;
                if (heart.tag.counter > 100) {
                    // カウンタが100を超えていたら削除する
                    heart.destroy();
                } else if (heart.tag.counter > 50) {
                    // カウンタが50を超えていたら半透明にしていく
                    heart.opacity = (100 - heart.tag.counter) / 50;
                    // このエンティティが変更されたという通知
                    heart.modified();
                }
            });
        }
        // イベント取得用に、ローカルエンティティを透明で作成
        var container = new g.E({
            scene: scene,
            x: 0,
            y: 0,
            width: g.game.width,
            height: g.game.height,
            touchable: true,
            local: true,
            parent: scene
        });
        // ローカルエンティティのpointDownトリガーに関数を登録
        container.pointDown.add(function(e) {
            // タッチされたらローカル情報と座標情報を基にイベントを生成
            g.game.raiseEvent(new g.MessageEvent({
                type: "generate-heart",
                color: color,
                x: e.point.x - scene.assets["heart"].width / 2,
                y: e.point.y - scene.assets["heart"].height / 2
            }));
        });
        // Messageイベントを受け取るためのハンドラを登録
        scene.message.add(function(e) {
            // イベント種別を見て
            var data = e.data;
            if (data.type === "generate-heart") {
                // 色と座標情報を基にハートを作成
                generateHeart(
                    data.x,
                    data.y,
                    data.color
                );
            }
        });
        // ---- ここからコントロールパネル。基本は全てローカルエンティティとして処理
        // コントロールパネルを作成。クリッピングをするためPaneを使う
        var controlPanel = new g.Pane({
            scene: scene,
            x: g.game.width - 48,
            y: 32,
            width: 32,
            height: 32,
            local: true,
            tag: {
                expand: false
            },
            parent: scene
        });
        // コントロールパネルの開閉スイッチを配置
        var panelSwitch = new g.FilledRect({
            scene: scene,
            x: 0,
            y: 0,
            width: 32,
            height: 32,
            cssColor: "#ccc",
            local: true,
            touchable: true,
            parent: controlPanel
        });
        // 選択されているツールにつける枠
        var activeTool = new g.FilledRect({
            scene: scene,
            x: 0, // 座標はオープン時に計算するので仮
            y: 0,
            cssColor: "#f79",
            width: 34,
            height: 34,
            opacity: 0.5,
            local: true,
            parent: controlPanel
        });
        // コントロールパネルに表示するツールアイコンを作成する関数
        function createTool(assetId, y) {
            var tool = new g.Sprite({
                scene: scene,
                src: scene.assets[assetId],
                x: 8,
                y: y,
                parent: controlPanel,
                local: true,
                touchable: true,
                tag: {
                    selected: true,
                    assetId: assetId
                }
            });
            // ツールアイコンは使いまわすのでリサイズしておく
            tool.width = 32;
            tool.height = 32;
            tool.invalidate();
            // このツールを選択した結果を反映
            tool.pointDown.add(function(e) {
                color = tool.tag.assetId;
                activeTool.x = e.target.x - 1;
                activeTool.y = e.target.y - 1;
                activeTool.modified();
            });
            return tool;
        }
        var tools = [];
        // 用意するツールは三種類
        tools.push(createTool("heart", 48));
        tools.push(createTool("spade", 96));
        tools.push(createTool("sparkle", 144));
        // 開閉スイッチに触れたらコントロールパネルを開く
        panelSwitch.pointDown.add(function(e) {
            controlPanel.tag.expand = !controlPanel.tag.expand;
            if (controlPanel.tag.expand) {
                // 展開する
                controlPanel.height = 192;
                controlPanel.width = 48;
            } else {
                // 折りたたむ
                controlPanel.height = 32;
                controlPanel.width = 32;
            }
            controlPanel.invalidate();
        });
        // ここまでコントロールパネル ----
        // 最初に中心にハートを出現させる
        generateHeart(
            g.game.width / 2 - scene.assets["heart"].width / 2,
            g.game.height / 2 - scene.assets["heart"].height / 2,
            color
        );
    });
    g.game.pushScene(scene);
}
module.exports = main;

コードを見る前にまずはlocalという機能についての解説ですが、localフラグを立てたエンティティを作成するとその実行端末でのみ描画される処理を作成できます。

このlocalフラグを立てたエンティティをタッチした、というイベントも、その端末でのみ発生します。

localエンティティを使ってその端末固有の表示を行い、localイベントを処理するようにすれば、視聴者の手元でだけ操作できるコントロールパネルを実現できます。

コントロールパネルの処理は中略しますが、以下がコードで重要な部分になります。

// コントロールパネルの開閉スイッチを配置
var panelSwitch = new g.FilledRect({
    scene: scene,
    x: 0,
    y: 0,
    width: 32,
    height: 32,
    cssColor: "#ccc",
    local: true,
    touchable: true,
    parent: controlPanel
});
// ~中略
    // このツールを選択した結果を反映
    tool.pointDown.add(function(e) {
        color = tool.tag.assetId;
        activeTool.x = e.target.x - 1;
        activeTool.y = e.target.y - 1;
        activeTool.modified();
    });
// 開閉スイッチに触れたらコントロールパネルを開く
panelSwitch.pointDown.add(function(e) {
    controlPanel.tag.expand = !controlPanel.tag.expand;
    if (controlPanel.tag.expand) {
        // 展開する
        controlPanel.height = 192;
        controlPanel.width = 48;
    } else {
        // 折りたたむ
        controlPanel.height = 32;
        controlPanel.width = 32;
    }
    controlPanel.invalidate();
});
// ここまでコントロールパネル ----

panelSwitch変数をlocalフラグ付きで作成しています。これで、panelSwitchのpointDownトリガーはローカルイベントしか発生しないので、その端末でのみ開閉する事ができます。

開閉した結果、選択したツールを変更していますが、これもlocalイベントとして処理されるのでその端末でのみ処理されます。

ただし、このままですとハートを出現させるコードにこのコントロールパネルの操作を反映できません。反映させるためには、やはりlocalエンティティを利用した一工夫が必要です。以下に今回作成する対象のコードを示します。

// イベント取得用に、ローカルエンティティを透明で作成
var container = new g.E({
    scene: scene,
    x: 0,
    y: 0,
    width: g.game.width,
    height: g.game.height,
    touchable: true,
    local: true,
    parent: scene
});
// ローカルエンティティのpointDownトリガーに関数を登録
container.pointDown.add(function(e) {
    // タッチされたらローカル情報と座標情報を基にイベントを生成
    g.game.raiseEvent(new g.MessageEvent({
        type: "generate-heart",
        color: color,
        x: e.point.x - scene.assets["heart"].width / 2,
        y: e.point.y - scene.assets["heart"].height / 2
    }));
});

これまでは、SceneのpointDownCaptureトリガーを利用して入力操作で直接ハートを発生させていましたが、localのEを全面に出し、そこのpointDownトリガーで一度イベントをキャッチした後、raiseEventという命令でイベントを加工して送信しています。

これにより、localでタッチイベントを発生させ、それを契機に発生させるグローバルなMessageEventにローカルの色情報を付与しています。

このイベントを受信するため、Sceneのmessageトリガーを利用して、イベントを受信した場合にそのイベントに含まれる色情報を抽出し、ハートを生成しています。

// Messageイベントを受け取るためのハンドラを登録
scene.message.add(function(e) {
    // イベント種別を見て
    var data = e.data;
    if (data.type === "generate-heart") {
        // 色と座標情報を基にハートを作成
        generateHeart(
            data.x,
            data.y,
            data.color
        );
    }
});

generateHeartを呼び出しているところは色情報を渡しているところくらいが相違点です。

これで、その端末の固有UIと固有イベント、それを基にしたグローバルなイベントという処理を作成できました。

今回は視聴者がハートやスペードを出せるようにする程度でしたが、応用すると対戦や協力ゲームなども作ることができます。

マルチプレーについての今後の見込み

マルチプレーが作れるようになると、対戦ゲームやビンゴなどもユーザの皆様が自作できるようになっていきます。

ただ、現状ですと制作環境の提供が間に合っておらず、ハートを出すツールのようなデバッグしなくても成立するようなツール以外は、制作していただくのがなかなか難しい状態です。

現在、Akashicの開発チームでは、この状態を解消できるように制作環境の提供作業を進めています。

遅くても、2019年の1月中には公開できると思いますので、今しばらくお待ちください。

参考資料

この記事で作成されたハートを出すツールのマルチプレー版のソースコードは以下に公開されています。

また、サンプルとして実験放送でも利用できるようにしています。ニコニコ新市場の自作ゲームタブに登録してありますので、実験放送で利用の上、どんなものかを見ていただければ。

また、今まで言及していませんでしたが、以下のコンテンツ群もマルチプレー対応版として制作されたものです。 ソースコードも公開されていますので、改造の元ネタや参考資料としてご参照ください。