ゆるっとゲーム制作 - フィールド作成

ゆるっとゲーム制作、今回はフィールドの作成です。

 

さて、どんなゲームを作るのかですが、皆さんApex Legends(エーペックス)というゲームはご存じでしょうか?FPSでバトルロワイヤル形式のマルチプレイ対戦ゲーム、フィールドを駆け回りながら銃を撃って相手を倒しつつ最後の一人まで生き残れば勝ち的なやつですね。バトロワ系のゲームって操作が難しいイメージがあったんですが、筆者は知人に「Apexはそれ系のゲームの中ではカジュアルにできる方だよ」と聞いて、始めてみたわけです。

 

「...難しいやんけ!」

 

スプラトゥーンみたいなカジュアルさをイメージしていた自分には難しすぎました。武器の違いもよくわからないし、弾も撃ってみたものの当たってる気がしない。それでも他と比べるとカジュアルな方だというのは間違ってはないと思うんですが、もっと敷居が低いものはないだろうか。。

 

はい、ということで「すっごいカジュアルなバトロワマルチプレイ対戦ゲーム」の制作を目指してみます。マルチプレイ、というところはネックに思えそうですが、便利アセットでなんとかします。

 

そんな感じで作るものも決まったところで話をフィールド作成に戻します。今回使用するアセットは「TileWorldCreator3」です。

assetstore.unity.com

 

どんなアセットかはAsset Storeの動画を見るとわかりますが、いくつかの設定をしてボタンを押すと、3Dのキュービックなフィールドを自動で生成してくれます。使い方は以下の動画の通りですぐできちゃいます。

www.youtube.com

 

こんな感じで出来ました。

f:id:tota66:20220110190029p:plain

 

この出来たフィールドのGameObjectなんですが、そのままPrefabとして保存しようとするとメッシュの参照が外れて上手くいきません。Prefab化するには、「FBX Exporter」を使って、メッシュをFBX形式で保存します。FBX ExporterはPackageManagerからインポートしましょう。

docs.unity3d.com

 

インポート後、作ったフィールドのGameObjectを右クリックで選択して「Convert To FBX Prefab Variant...」を選択、設定パネルで「Convert」を押すとFBXとPrefabを同時に作成してくれます。

「Export To FBX...」の方を選んでFBXのみ作成することもできますが、FBXはTransformの編集などが出来ないため、Prefabを作っておいてそちらを弄るようにした方が扱いやすいかと思います。

f:id:tota66:20220110190533p:plain

FBXとPrefabのエクスポート

 

はい、こんな感じでフィールドのPrefabができました。次回はこのフィールドをステージとして読み込んでみましょうかね。ではまた〜

 

ゆるっとゲーム制作 - ステート管理1

明けましておめでとうございます!久々の投稿です。

 

最近仕事ではUnityを使うことが多いんですが、年末年始に個人的にアセットを色々と買って試したりしたのでいくつか記事にしようかと思っています。Unityは便利なアセットがたくさんあるので、なるべくアセットに頼って少ない労力でゲームを作るのを目標にします。続けられるかわかりませんが、ゆるゆるとやっていきましょう〜!

 

さて、ゲームを作るにあたりどこから始めるかというところなんですが、何を作るにせよまず骨格を決めていきましょう。Arborというアセットを使ってみます。

assetstore.unity.com

 

ゲームはタイトルから始まって、ステージ選択、ステージ、、みたいに場面が切り替わっていくわけなんですが、それら一つ一つを「ステート」として管理して遷移をビジュアライズできるのがこのアセットです。似たようなものにPlayMakerなんてアセットもありますが、Arborの方がステート管理に特化してシンプルなイメージです。

 

Arborにはステート管理以外にも、ビヘイビアツリーというNPCのAIを組むのに使えそうな機能や、データフローというデータの受け渡しをGUIで組めるような機能があるんですが、これらは一旦置いておきます。これらは場合によっては便利かもと思いつつ、スクリプトで書いた方が楽かもな〜と思うところはあります。

 

動作は以下のような感じ。

ステートのフローをGUIで組んだ後、実行すると、各ステートにアタッチしたスクリプトに定義されたイベント関数(OnStateBegin, OnStateEnd等)が呼ばれるようになってます。

f:id:tota66:20220108184017g:plain

Arbor Editor 動作の様子

あー遷移してるな、というのが目でわかります。遷移の組み替えは繋ぎ先を変えればいいので楽にできそうですね。まあでも、これを使うことの一番の利点はステートごとに処理を記述するという骨格ができ、後々のコードの書き方が整理されることかなと思います。

 

ちなみにこのステートたちはそれぞれ子にサブステートを持つことができ、要は木のような構造をとれます。この辺の機能もあるのが便利なところです(自前でこの辺の仕組みまで作ろうとするとちょっと面倒そうですし)。この辺りの話やもうちょい具体的な使い方などについてはまた別の機会に紹介しようかなと思います。では〜

 

AGC033

atcoder.jp
GWもあと少し。switchを少し前に買ったんですが、ちょっと誘惑に負けるとGWが溶けそうでやばいです。

AGCはAの1完。Aは幅優先探索かなーと一度Pythonで書いたんですが、どうしてもTLEで通らなさそうで。。仕方なくC++で書き直したんですが、C++忘れすぎていて30分くらいコンパイルエラーで苦しむことに。queueもx, y別々にしたけどまとめられたよなぁとか(pair<int, int>だった)、色々あったけど何とか通りました。

#include <iostream>
#include <string>
#include <deque>
using namespace std;

int main() {
    int W, H;
    cin >> H >> W;

    int dist[H][W];
    deque<int> xvec;
    deque<int> yvec;

    for (int i = 0; i < H; ++i) {
        string s;
        cin >> s;
        for (int j = 0; j < W; ++j) {
            if (s[j] == '#') {
                dist[i][j] = 0;
                xvec.push_back(j);
                yvec.push_back(i);
            } else {
                dist[i][j] = -1;
            }
        }
    }

    int max_dist_count = 0;
    while (xvec.size() != 0) {
        int y = yvec.front();
        yvec.pop_front();
        int x = xvec.front();
        xvec.pop_front();
        int dist_count = dist[y][x] + 1;
        if (x < W - 1 && dist[y][x+1] == -1) {
            dist[y][x+1] = dist_count;
            yvec.push_back(y);
            xvec.push_back(x+1);
            max_dist_count = dist_count;
        }
        if (y < H - 1 && dist[y+1][x] == -1) {
            dist[y+1][x] = dist_count;
            yvec.push_back(y+1);
            xvec.push_back(x);
            max_dist_count = dist_count;
        }
        if (0 < x && dist[y][x-1] == -1) {
            dist[y][x-1] = dist_count;
            yvec.push_back(y);
            xvec.push_back(x-1);
            max_dist_count = dist_count;
        }
        if (0 < y && dist[y-1][x] == -1) {
            dist[y-1][x] = dist_count;
            yvec.push_back(y-1);
            xvec.push_back(x);
            max_dist_count = dist_count;
        }
    }

    cout << max_dist_count;
}

Bは時間切れ後にちょっと考えて、先手はひたすら左、後手はひたすら右に動かすみたいなシミュレーションを4方向それぞれスタートから行う方法で通りましたが、解説聞くとどうもそれは真の解答ではないらしいという。。

こういう、最終的な状態(今回は、駒がどこかにいて生存しているかor途中どこかで落とされるか)への辿り着き方が、途中の状態が色々ありえてよくわからない場合は逆から考えるというのがセオリーなようです。グラフ理論的な、状態一つ一つがノードになったものをイメージしてみると、自分の今いるノードがゴールに辿り着くのか、スタートから考えるとわからないまま進むことになるわけですが、ゴールから辿ると確実にゴールに着くノードを全て洗い出すことができます。

今回は、最後の各マス上で生存している状態をゴールとして逆から辿っていって、スタートの状態にすることができれば生存ルートがあるということで後手勝ち、という塩梅のようでした。いやーむずいっす。シミュレーションでの解答も一見良さそうだけどどういう反例があるんだろうか。