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

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

 

さて、どんなゲームを作るのかですが、皆さん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 動作の様子

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

 

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

 

アナザーエデンコンサートに行ってきた

あけましておめでとうございます。本年もよろしくお願いいたします。

 

昨日、アナザーエデンというゲームのコンサートに誘ってもらい行ってきたんですが、とても良かったです!

https://www.jvcmusic.co.jp/another-eden/concert/

 

実はゲームをやったことがなかったので、当日始めて予習するというひどい有様だったんですが...

"a refrain of times" というコンサートタイトル通り、

タイトルBGMから始まって旅を最初から振り返っていくような構成になっており、

超にわかの自分にも「あ、これは最初の村のBGMだ!」などとすぐわかることができました。

途中からは未知のゾーンに入るわけですが、それはそれでどういうシーンなのかなと想像を膨らませながら楽しむことができました。

 

情景が浮かぶようなフィールド曲、激しいボス曲など色々あったんですが、

個人的にはアンコールの一番最後、

ゆっくり物語を閉じていくような優しい曲が流れた時にちょっと感動してしまいました。

きっと良いゲームというのは終わった後にもユーザの心にそっと残って、それこそふとした時にリフレインしてくるようなものなんだろうと思います。

最後のは、そんなユーザそれぞれの冒険の思い出が心の中にそっと留まってくれたら、という作り手の思いがこもった曲のように感じました

(知らないだけで全く違う場面の曲なのかもしれないですが...w)

 

自分も、何か形になるものを作れるように今年も頑張らなくてはー

  

ABC127

atcoder.jp
土曜にあったやつです。
30分遅れで初めて、A-Cの3完。

Dは時間内では、カードをソートして入れ替えて、をM回やるTLE待ったなしな方法しか思いつかなくてダメでした。
後に上書きされるような小さい数のBを入れ替えるという操作は、無駄になるわけですね。

その後Aを小さい順ソート、BjをCj回並べた(j=[1, M])配列BCを大きい順にソートしてAとBCをの要素を大きい方で入れ替える、
という方法を思いついたけど、これもTLE。
うーん。。と思ったけどこれはBCが大きくなりすぎて起こってたもので、BCの要素数をNまでにしとけばOKでした。

N, M = map(int, input().split())

A = list(map(int, input().split()))

count = 0
BC = []
for i in range(M):
    b, c = map(int, input().split())
    BC.append((b, c))

A.sort()
BC.sort(reverse=True, key=lambda x: x[1])

count = 0
CArr = []
for b, c in BC:
    if count >= N:
        break
    for i in range(b):
        CArr.append(c)
        count += 1
        if count >= N:
            break

for i in range(len(CArr)):
    if A[i] < CArr[i]:
        A[i] = CArr[i]

ans = sum(A)
print(ans)

E、Fは難しそうだったのでスルー。。ひとまず4完目指したい

拡張メソッド

Unityで

「一つ下の子要素のコンポーネントを全てとってきたいけど、GetComponentsInChildren()を使うと孫要素以下まで再帰的にとってきちゃう。一つ下だけでいいのに!」

というような、ありそうで微妙にない関数が欲しいというような時は、C#の拡張メソッドの機能で定義してやると便利です。

internal static class TransformExtension
{
    public static List<T> GetComponentsInFirstChildren<T>(this Transform transform) where T : MonoBehaviour
    {
        var list = new List<T>();
        for (var i = 0; i < transform.childCount; ++i)
        {
            var c = transform.GetChild(i).GetComponent<T>();
            if (c != null)
            {
                list.Add(c);
            }
        }

        return list;
    }
}

こういうのを適当なところ(自分はExtensions.csというファイルにまとめてます)に書くと、呼び出し側は

var components = transform.GetComponentsInFirstChildren<SomeClass>();

という感じ。スッキリ書ける。

インデックスバリデーション付きのListとか、色々なところで使いそうなやつは積極的に定義しとくと良さそう。

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

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

ABC125

atcoder.jp
ABDの3完。Dが簡単だったみたいですぐできたんですが、CでTLEから逃れられず。。

Cは、数を書き直せるとは数を消すと同じ、というところまではわかって、あとは残りの数のgcd(最大公約数)をどう早く解くかなんですが、これも累積和と同じような感じで予め求めておけるんですね〜
消す数をA[i]とした時、その左側のgcdをL[i]、右側のgcdをR[i+1]として、

N = int(input())
A = list(map(int, input().split()))


def gcd(a, b):
    if a < b:
        a, b = b, a
    if b == 0:
        return a
    return gcd(b, a % b)


L = [0] * (N + 1)
R = [0] * (N + 1)
for i in range(N):
    L[i+1] = gcd(L[i], A[i])
    R[N-i-1] = gcd(R[N-i], A[N-i-1])

ans = 1
for i in range(N):
    ans = max(ans, gcd(L[i], R[i+1]))

print(ans)

LとRで分けて最後にgcd(L, R)と計算できること、L, Rの要素が漸化式で高速に求められることがミソですか〜
全完狙ってたけどおあずけ...