この記事について

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

Akashic Engineで作成したコンテンツをRPGアツマールに投稿し、ニコニコ新市場に登録申請をする事で、実験放送で投稿したツールやゲームを実行できます。これを、ニコニコ新市場対応コンテンツと呼びます。

本記事では、ニコニコ新市場対応コンテンツの作り方でよくある質問に回答するための取り組みの最初の一つとして、衝突判定について記したいと思います。

衝突判定とは

衝突判定は、大きく二つに分けられると思います。

  • 物体Aと物体Bの衝突
  • ポインターと物体の衝突

後者については、ゲームを作ってみよう でもコードとしては公開していますが、解説を省略しているので、少し掘り下げて解説させていただきます。

事前準備

泥棒バスターを改造してみよう の記事での手順を使って、まずは環境を構築します。

作った環境を基に、ゲームを作るところからやっていきます。

以下は、CUIツールでの実行の様子です。

$ mkdir collision
$ cd collision
$ akashic init
INFO: copied .eslintrc.json.
INFO: copied README.md.
INFO: copied audio.
INFO: copied game.json.
INFO: copied image.
INFO: copied package.json.
INFO: copied script.
INFO: copied text.
prompt: width: (320) 640
prompt: height: (320) 360
prompt: fps: (30)
INFO: Done!
$ npm i

基本的に手順はツールを作ってみようと同様です。

最後に「npm i」というコマンドの実行が追加されていますが、これはニコニコ新市場対応コンテンツでの躓きポイントと対策集で触れたnpm run lint等を実行するための下準備です。

ここまで、定型として扱っていただくのをおすすめします。

エンティティをタッチする

まずは、簡単な「ポインターと物体の衝突」から行きます。公式のチュートリアルはこちらです。

要約すると「touchableプロパティをtrueにして、pointDownトリガー等でタッチした事を検知できる」事になります。

akashic initで作られるサンプルコードを少し改造して、右に移動する四角をタッチしてランダムな場所にジャンプするコードを書いてみます。

function main(param) {
    var scene = new g.Scene({game: g.game});
    scene.loaded.add(function() {
        // 以下にゲームのロジックを記述します。
        var rect = new g.FilledRect({
            scene: scene,
            cssColor: "#ff0000",
            width: 32,
            height: 32, // 末尾のカンマのつけ忘れに注意
            touchable: true // これを忘れずに追加
        });
        // pointDownトリガーを利用してエンティティとポインティングデバイスの接触判定を行う
        rect.pointDown.add(function(e) {
            // 画面の大きさの左半分のどこかに移動させる
            rect.moveTo(
                g.game.random.get(0, g.game.width / 2 - rect.width),
                g.game.random.get(0, g.game.height - rect.height)
            );
            rect.modified();
        });
        rect.update.add(function () {
            // 以下のコードは毎フレーム実行されます。
            rect.x++;
            if (rect.x > g.game.width) rect.x = 0;
            rect.modified();
        });
        scene.append(rect);
    });
    g.game.pushScene(scene);
}
module.exports = main;

d968580d938580106f5cd3598dea065b6930f386

重要な点は以下の通りで、touchableをtrueにして、そのtouchableをtrueにしたエンティティのpointDownを利用して、所定の処理を記述しています。

    touchable: true // これを忘れずに追加
});
// pointDownトリガーを利用してエンティティとポインティングデバイスの接触判定を行う
rect.pointDown.add(function(e) {
    // 画面の大きさの左半分のどこかに移動させる
    rect.moveTo(
        g.game.random.get(0, g.game.width / 2 - rect.width),
        g.game.random.get(0, g.game.height - rect.height)
    );
    rect.modified();
});

他にも色々やり方はありますが、これだけ覚えておけばボタン等は作れると思いますので、ご活用ください。

エンティティ同士の衝突の事前準備

続いて、エンティティ同士の衝突判定に移りますが、その前にまずは衝突しない、衝突してほしいコードを記述しましょう。

タップした場所が画面の左半分であれば赤い四角を配置し右に動かし、タップした場所が画面の右半分であれば青い四角を配置し左に移動するようにするコードです。

function main(param) {
    var scene = new g.Scene({game: g.game});
    scene.loaded.add(function() {
        function generateRect(x, y) {
            // 半分より右かを判定
            var right = x > g.game.width / 2 + 16;
            var rect = new g.FilledRect({
                scene: scene,
                // 半分より右なら青、左なら赤
                cssColor: right ? "#0000ff" : "#ff0000",
                x: x - 16,
                y: y - 16,
                width: 32,
                height: 32
            });
            rect.update.add(function () {
                // 半分より右なら左に進み、左なら右に進む
                rect.x += right ? -1 : 1;
                rect.modified();
                // 画面からはみ出たら削除
                if (rect.x > g.game.width) {
                    rect.destroy();
                } else if (rect.x < -32) {
                    rect.destroy();
                }
            });
            return rect;
        }
        // 任意の場所をタップしたら四角を生成する
        scene.pointDownCapture.add(function(e) {
            var rect = generateRect(e.point.x, e.point.y);
            scene.append(rect);
        });
    });
    g.game.pushScene(scene);
}
module.exports = main;

ef74b41160615742ba8a72899b1d819ea6b394e9

赤と青を接触させたくなる絵になってきました。

いくつかの方法がありますので、代表的な方法を2点解説させていただきます。

Collisionモジュールの利用

Akashic Engineには、デフォルトで接触判定に利用可能なCollisionモジュールというものが用意されています。公式のリファレンスは下記にあります。

リファレンスにあるCommonAreaという型に馴染みが無いかもしれません。
リファレンスの見方としては、「Implemented by」に書かれているものが、本インターフェースを実装しているという記述になります。

Akashic Engineのエンティティは基底のクラスにEを採用しており、CommonAreaの「Implemented by」にはEが入っているので全てのエンティティはCommonAreaの実装という扱いになります。ということで、CollisionモジュールのintersectAreas関数に、判定したい二つのエンティティを入れれば接触判定させることができます。

こちらを利用し、接触したら削除する、というコードを入れてみたのが以下のコードになります。

function main(param) {
    var scene = new g.Scene({game: g.game});
    scene.loaded.add(function() {
        function generateRect(x, y) {
            // 半分より右かを判定
            var right = x > g.game.width / 2 + 16;
            var rect = new g.FilledRect({
                scene: scene,
                // 半分より右なら青、左なら赤
                cssColor: right ? "#0000ff" : "#ff0000",
                x: x - 16,
                y: y - 16,
                width: 32,
                height: 32,
                tag: {
                    right: right
                }
            });
            return rect;
        }
        // 任意の場所をタップしたら四角を生成する
        scene.pointDownCapture.add(function(e) {
            var rect = generateRect(e.point.x, e.point.y);
            scene.append(rect);
        });
        // 接触判定と当たり判定を毎フレーム実施する
        scene.update.add(function() {
            // 全エンティティの接触を検査
            for (var i = 0; i < scene.children.length - 1; i++) {
                var current = scene.children[i];
                var isDestroy = false;
                // 接触していればで判定
                for (var j = i + 1; j < scene.children.length; j++) {
                    // Note: ここでtag.rightが同一であればスキップとかやってもよい
                    // CollisionモジュールのintersectAreasで接触判定
                    if (g.Collision.intersectAreas(current, scene.children[j])) {
                        // 検査対象と接触しているエンティティは削除
                        scene.children[j].destroy();
                        j--;
                        isDestroy = true;
                    }
                }
                if (isDestroy) {
                    // 接触していれば、検査対象も削除
                    current.destroy();
                    // destroyを呼ぶとchildrenからも削除されるのでiを1減らす
                    i--;
                }
            }
            // 残ったエンティティだけ移動させる
            scene.children.forEach(function(rect) {
                // 半分より右なら左に進み、左なら右に進む
                rect.x += rect.tag.right ? -1 : 1;
                rect.modified();
                // 画面からはみ出たら削除
                if (rect.x > g.game.width) {
                    rect.destroy();
                } else if (rect.x < -32) {
                    rect.destroy();
                }
            });
        });
    });
    g.game.pushScene(scene);
}
module.exports = main;

入れ子のfor文などがあり、コード全体としては少し長くなっていますが、肝は以下のコードになります。関数呼び出し一つで接触判定ができている様子が確認できます。

// CollisionモジュールのintersectAreasで接触判定
if (g.Collision.intersectAreas(current, scene.children[j])) {
    // 検査対象と接触しているエンティティは削除
    scene.children[j].destroy();

Util.distance

前のコードでも当たり判定はできていますが、少しでも当たれば削除されるというのは、実運用上シビアにすぎるかもしれません。

次は、この処理を改変し、「接触した時、中心点からの距離が一定以内なら消滅、かすっているだけなら大きさが減少」という改造をしたいと思います。

これも、Akashic Engineではデフォルトでg.Util.distanceという関数が用意されています。

こちらを利用して実装してみます。全コードは以下の通りです。

function main(param) {
    var scene = new g.Scene({game: g.game});
    scene.loaded.add(function() {
        function generateRect(x, y) {
            // 半分より右かを判定
            var right = x > g.game.width / 2 + 16;
            var rect = new g.FilledRect({
                scene: scene,
                // 半分より右なら青、左なら赤
                cssColor: right ? "#0000ff" : "#ff0000",
                x: x - 16,
                y: y - 16,
                width: 32,
                height: 32,
                tag: {
                    right: right
                }
            });
            return rect;
        }
        // 任意の場所をタップしたら四角を生成する
        scene.pointDownCapture.add(function(e) {
            var rect = generateRect(e.point.x, e.point.y);
            scene.append(rect);
        });
        // 接触判定と当たり判定を毎フレーム実施する
        scene.update.add(function() {
            // 全エンティティの接触を検査
            for (var i = 0; i < scene.children.length - 1; i++) {
                var current = scene.children[i];
                var damage = 0;
                // 接触していればで判定
                for (var j = i + 1; j < scene.children.length; j++) {
                    // Note: ここでtag.rightが同一であればスキップとかやってもよい
                    // CollisionモジュールのintersectAreasで接触判定
                    var target = scene.children[j];
                    if (g.Collision.intersectAreas(current, target)) {
                        // 検査対象と接触しているエンティティは削除
                        var distanceByCenter = g.Util.distance(
                            current.x + current.width / 2,
                            current.y + current.height / 2,
                            target.x + target.width / 2,
                            target.y + target.height / 2
                        );
                        // 接触した距離の大きさに応じてダメージを計算
                        var currentDamage = 32 - distanceByCenter;
                        // ダメージが無ければ処理を中断
                        if (currentDamage > 0) continue;
                        // 元の4分の1以上の近さであれば致命傷とする
                        if (distanceByCenter < 8) {
                            currentDamage = 32;
                        }
                        damage += currentDamage;
                        // ダメージ分大きさを小さくする
                        target.width -= currentDamage;
                        target.height -= currentDamage;
                        // 一定以上大きさが小さくなっていたら削除
                        if (target.width < 4) {
                            target.destroy();
                            j--;
                        }
                    }
                }
                if (damage > 0) {
                    // 検査対象は累計ダメージを減算
                    current.width -= damage;
                    current.height -= damage;
                    // 一定以上大きさが小さくなっていたら削除
                    if (current.width < 4) {
                        current.destroy();
                        // destroyを呼ぶとchildrenからも削除されるのでiを1減らす
                        i--;
                    }
                }
            }
            // 残ったエンティティだけ移動させる
            scene.children.forEach(function(rect) {
                // 半分より右なら左に進み、左なら右に進む
                rect.x += rect.tag.right ? -1 : 1;
                rect.modified();
                // 画面からはみ出たら削除
                if (rect.x > g.game.width) {
                    rect.destroy();
                } else if (rect.x < -32) {
                    rect.destroy();
                }
            });
        });
    });
    g.game.pushScene(scene);
}
module.exports = main;

まず、以下のコードで距離を計算しています。distanceは点と点の距離を計算する関数なので、それぞれのエンティティの中心座標を与えています。

// 検査対象と接触しているエンティティは削除
var distanceByCenter = g.Util.distance(
    current.x + current.width / 2,
    current.y + current.height / 2,
    target.x + target.width / 2,
    target.y + target.height / 2
);

次に、距離の応じたダメージを計算しています。(余談ですが32という数値はさすがに定数なりにした方がよいと思いましたが、記事の趣旨とずれるので割愛しました)

// 接触した距離の大きさに応じてダメージを計算
var currentDamage = 32 - distanceByCenter;
// ダメージが無ければ処理を中断
if (currentDamage > 0) continue;
// 元の4分の1以上の近さであれば致命傷とする
if (distanceByCenter < 8) {
    currentDamage = 32;
}

ダメージ分大きさを減らし、一定以上小さくなれば削除します。

// ダメージ分大きさを小さくする
target.width -= currentDamage;
target.height -= currentDamage;
// 一定以上大きさが小さくなっていたら削除
if (target.width < 4) {
    target.destroy();
    j--;
}

実行すると、衝突によって小さくなる、一定以上小さくなると消滅する、対戦ゲームが作れそうなものが出来上がっている様子がわかると思います。

e16a7f58aa83c676f449ff9d7460b7cf6f3c7955

単純に中心点からの距離のみで計算しているため、ゲームとして仕上げるには小さい四角形の攻撃力が高すぎるなどの問題がありますが、この記事はゲームとして完成させることを目的としていないため、仕上げについては割愛します。

Akashic Engineのバージョンについて

距離をはかる方法は、本当はもう少し便利な方法があります。

本記事執筆時点では、Akashic Engine本体の不具合によって正常に動作しないため、別の方法で代替しました。

利用しているAkashic Engineのバージョンによっては、これらの関数を呼び出す事で、より簡単に書けるようになると思いますので、バージョンアップにご期待ください。

終わりに

今回は、Twitterでいただいた要望を基に記事を書くという試みをしてみました。

作成したソースコードは以下のでも公開されていますので、よければご参照の上、コンテンツの投稿にお役立てください。

今後もこういった形で、逐次情報出しをしていきますので、引き続きよろしくお願いします。

記事を書くきっかけにもなりますので、Twitterの公式アカウントにも、お気軽にご要望をお寄せください。