バナナラボ

Vtuber美作ワニのブログです

【バナナラボ通信】Vol.3『2日間ゲーム作ってみた』の解説と感想

こんばんは、バナナラボの美作です。
今日は以前YouTubeに投稿したゲーム制作の解説をしようと思います。

動画は以下から見れます www.youtube.com

ローグライクを2日間で作り上げるためのポイント

今回、ローグライクという難しいジャンルを二日間で作りきったわけですがそれをどういう方法で実現させたのか解説します。

ロジックについて

ダンジョン生成敵の行動機能さえやっつけることができればそう難しくはありません。
また戦闘システムは最悪時間がなければ簡素なものにしようと考えていたので最初にダンジョン生成から着手することにしました。

1日目の実装

ダンジョン生成 - 部屋生成

ダンジョン生成はあまり難しく考えたくなかったのでそれっぽく作ることをまず目標にしました。

このように、予め部屋のブロック配置が決まるようにします。パターンは2×2や3×3の部屋などをそれぞれ用意します。
階層によってそのパターンが変わっていくようにします。
部屋の大きさは最小・最大を決めて縦と横の長さをランダムで決定し、それぞれの部屋のサイズを決めていきます。
実際のソースコードは以下のように書いてます。

// ブロックの配置
var blocks = new List<BlockParameter>();
var gapX = 0;
var gapZ = 0;
for (var x = 0; x < chunkCount.xCount; x++)
{
    for (var z = 0; z < chunkCount.zCount; z++)
    {
        var area = Areas.First(v => v.AreaX == x && v.AreaZ == z);
        for (var bX = 0; bX < area.LengthX; bX++)
        {
            for (var bZ = 0; bZ < area.LengthZ; bZ++)
            {
                blocks.Add(new BlockParameter(bX + gapX, bZ + gapZ, area.AreaNumber));
            }
        }

        gapZ += Areas
            .Where(v => v.AreaZ == z)
            .OrderByDescending(v => v.LengthZ)
            .First().LengthZ + 5;
    }

    gapX += Areas
        .Where(v => v.AreaX == x)
        .OrderByDescending(v => v.LengthX)
        .First().LengthX + 5;
    gapZ = 0;
}

部屋のサイズが決まれば次は通路を作っていきます。

ダンジョン生成 - 通路生成

部屋ができて、各部屋のサイズが決定ができたので通路を作ります。
通路のロジックも早く作ってしまいたいので手抜きで作ってしまいます。

通路を作る際に気をつけたいのは行くことが出来ない部屋が発生しないことです。
それを配慮する場合、全ての部屋で通路が必ず一本できるようにロジックを組めばその状態は回避できます。
図のように順番に部屋を巡っていき、通路がないところを候補としてランダムで通路を1本~最大本生成するようにする。
ソースコードは以下のように作りました。

// 通路の作成
var accesses = CreateAccesses(Areas);
var tBlocks = blocks.Where(x => x.AreaNumber >= 0);
foreach (var access in accesses)
{
    var ab = access
        .AreaNumbers
        .Select(x => Areas.First(v => v.AreaNumber == x))
        .ToArray();
    var a = ab[0];
    var b = ab[1];
    var aBlocks = tBlocks.Where(v => v.AreaNumber == a.AreaNumber);
    var bBlocks = tBlocks.Where(v => v.AreaNumber == b.AreaNumber);

    if (a.AreaX == b.AreaX)
    {
        var x = UnityEngine.Random.Range(
            Mathf.Max(
                aBlocks.First().X,
                bBlocks.First().X + 1
            ),
            Mathf.Min(
                aBlocks.OrderByDescending(v => v.X).First().X,
                bBlocks.OrderByDescending(v => v.X).First().X + 1
            )
        );

        var sZ = aBlocks.OrderByDescending(v => v.Z).First().Z + 1;
        var eZ = bBlocks.OrderBy(v => v.Z).First().Z;
        for (var z = sZ; z < eZ; z++)
        {
            blocks.Add(new BlockParameter(x, z, -1));
        }

        continue;
    }

    if (a.AreaZ == b.AreaZ)
    {
        var z = UnityEngine.Random.Range(
            Mathf.Max(
                aBlocks.First().Z,
                bBlocks.First().Z + 1
            ),
            Mathf.Min(
                aBlocks.OrderByDescending(v => v.Z).First().Z,
                bBlocks.OrderByDescending(v => v.Z).First().Z + 1
            )
        );

        var sX = aBlocks.OrderByDescending(v => v.X).First().X + 1;
        var eX = bBlocks.OrderBy(v => v.X).First().X;
        for (var x = sX; x < eX; x++)
        {
            blocks.Add(new BlockParameter(x, z, -1));
        }

        continue;
    }

    throw new Exception($"bug {access.AreaNumbers[0]}:{access.AreaNumbers[1]}");
}

ダンジョン生成の完成

この2つが出来てしまえば後はプレイヤーの配置やアイテムの配置を組んでいくだけで、ここはハイスピードでできます。
実際1日目の段階でプレイヤーの移動も含めて一通りのシステムを完成させることができました。

2日目

敵の生成

1日目は基本機能の実装をしたので2日目は敵の機能開発をします。
敵の機能開発は結構詰まってしまって夜までかかってしまいました。
実際に組んだソースコードは以下。

// コスト計算、三倍距離を目安にする。多すぎるコストは対象外。
max = (Mathf.Abs(to.x - from.x) + Mathf.Abs(to.z - from.z)) * 2;
if (max > 10)
{
    Result = new DungeonMaker.BlockParameter[0];
    return;
}

// Debug.Log($"search {to} <- {from}");
var fromBlock = dungeonMaker.Blocks.First(block => block.X == from.x && block.Z == from.z);
try
{
    var results = new List<RouteInfo>();
    Search(
        fromBlock,
        new RouteInfo
        {
            route = new List<DungeonMaker.BlockParameter> {fromBlock},
            consume = new List<DungeonMaker.BlockParameter> {fromBlock},
        },
        results);
    var result = results.OrderBy(v => v.route.Count).FirstOrDefault();
    Result = result != null ? result.route.ToArray() : new DungeonMaker.BlockParameter[0];
    // Debug.Log("route: " + string.Join(" -> ", Result.Select(xxx => $"({xxx.X}, {xxx.Z})")));
}
catch (Exception e)
{
    //   Debug.Log(e);
    Result = new DungeonMaker.BlockParameter[0];
}


void Search(DungeonMaker.BlockParameter _fromBlock, RouteInfo _route, List<RouteInfo> _routeInfos)
{
    if (max <= _route.route.Count)
    {
        return;
    }

    loop--;
    if (loop < 0)
    {
        // 計算かかりすぎ
        throw new Exception("計算数が多すぎる");
    }

    foreach (var __block in Four(_fromBlock)
        .Where(_v => !_route.consume.Any(__v => __v.X == _v.X && __v.Z == _v.Z)))
    {
        if (__block.X == to.x && __block.Z == to.z)
        {
            _route.route.Add(__block);
            _route.consume.Add(__block);
            max = _route.route.Count;
            _routeInfos.RemoveAll(_v => _v.route.Count > max);
            _routeInfos.Add(_route);
            return;
        }

        Search(__block, new RouteInfo
        {
            route = _route.route.Concat(new[] {__block}).ToList(),
            consume = _route.consume.Concat(new[] {__block}).ToList()
        }, _routeInfos);
    }
}

DungeonMaker.BlockParameter[] Four(DungeonMaker.BlockParameter t)
{
    var blocks = dungeonMaker.Blocks;
    var n = blocks.FirstOrDefault(v => v.X == t.X && v.Z == t.Z + 1);
    var e = blocks.FirstOrDefault(v => v.X == t.X + 1 && v.Z == t.Z);
    var s = blocks.FirstOrDefault(v => v.X == t.X && v.Z == t.Z - 1);
    var w = blocks.FirstOrDefault(v => v.X == t.X - 1 && v.Z == t.Z);
    // 敵は壁として扱う
    var fourBlocks = new List<DungeonMaker.BlockParameter> {n, e, s, w}
        .Where(v => v != null && !enemyMaker.Enemies.Any(enemy => enemy.X == v.X && enemy.Z == v.Z));
    return fourBlocks.ToArray();
}

実はこの実装、妥協した結果の実装になります。
探索の実装と言えばAstarなど採用をしたかったのですが1日で対応しないといけない、できれば半日で済ませたいという気持ちがあったため一旦自前で実装することに決めました。
結果的にその判断は失敗であり、負荷に耐えられないという結果を産んでしまったのでちゃんと組めばよかったなと後悔しました。

今回の実装は単純な探索ロジックを組み、移動済の候補は除外しながら最短経路を取得しました。結果、複数体のキャラになると負荷が高くなってしまい、大量の敵配置ができなくなるという本末転倒な結果になりました。
ここをうまくやろうと思うなら実装方法はもっとシンプルにしないといけませんし、既に時間が経ってからは完成期日が迫ってしまっていたので諦めてゲームとして支障のない負荷に留めるような実装に変えました。

感想

今回、2日間という短い期間だったのでシステムをしっかり組むことはせず、単調な実装方法をそれぞれとっていきました。
ダンジョン生成はうまくできましたし、ゲームとして破綻しないラインに乗せることはできたので個人的に良かったと思います。

一方的のロジックのように設計をちゃんとやったほうが短縮できたケースもありましたし、コードもやっぱ綺麗にかかないと駄目だなと思いました。
超短期間のゲーム作りでもロジック組みに多少時間を割くべきだと思いました

最後に

また機会があればチャレンジしようかなと思います。
反省点は結構多かったので、その反省点をどう活かして挽回するのか、を考えていきたいなと。
まだまだ成長できるとも感じたので、ガンガンやっていきます!

YouTubeやってます

www.youtube.com YouTubeやってます!よかったら登録してね!
ゲーム実況・ゲーム制作関係の動画投稿をやってます!!