Unityでスペースロボクラ製作中#01

Created: 2025/02/28

初めに

 皆さん初めまして。
 今日からプログラミング勉強の一環で制作中の【スペースロボクラ(仮)】の開発記録を記事として投稿することにした大学一年生(ギリギリ)です。この記事ではゲームの制作経過はもちろん、ゲームの企画や構想など様々なお話を発信させてもらおうと思っています。どうかよろしくお願いします。

今回の記事では
簡単な企画内容
現状の進捗
今後の課題


この三点をテーマに話していきます。

企画内容

私が初めにこのゲームの制作を思いついた経緯を簡潔に話します。
「今の乗り物クラフトゲームは複雑すぎる!」
「複雑な仕組みを極限まで削った乗り物ゲームを作ろう!」
「ついでに宇宙を舞台にすれば余計な物理演算も負担も減るぞ!」
「宇宙を舞台にしたゲームは少ないから差別化もできる!」
「それだけではまだ足りない気がするな……」
「そうだ!そこにプラスしてロボットを取り入れよう!」
「宇宙を舞台に自作の人型ロボが自由に飛び回り戦うスペースロボクラを作ろう!」


本当はもっと複雑なのですが大まかなあらすじはこのような感じです。

 宇宙を舞台に戦うロボットをテーマに、極力ゲーム内の仕組みを単純に、それでいて好きなものを好きなように作れるクラフトゲームを作る。それが今回のゲームの趣旨です。

代替テキスト
chatGPTに生成してもらったイメージ画像(こんな風にできたらいいなあ)

現状の進捗

 現在の進捗としては
「ブロックの設置、削除、回転」」 「機体の操作」 「スラスターによる推力システム」
 といった機能をすでに実現しています。さすがにまだ動くロボットのような複雑な乗り物は作れませんが、ただ宇宙空間を自由に動ける宇宙船程度ならすでに制作できる仕組みが完成しています。下記の画像は試しに戦闘機モドキを作って動かしているところです。

代替テキスト
エディットモード
代替テキスト
プレイモード

 このゲームではまずエディットモードで機体を組み立てていくところから始まります。
 
 Addボタンでブロック設置、Destoryボタンでブロック破壊モードに遷移でき、下の三つのボタンでブロック。スラスター、斜めブロックの三種類のブロックを設置できる形です。まだUI関係にまで手が回っていないためボタンオブジェクトを手動で雑に配置している形ですが将来的にはデータベースに保存されたブロックのリストを読み込んで自動でブロック一覧をまとめる仕組みなども整備するつもりです。

 回転はボタン操作にするとマウスの負担が大きすぎるのでQ・Eキーに割り当て、左右に90度回転する形になります。斜めブロックの角度などはこのボタンで調整します。

 ロボットが完成した後にPlayボタンを押すと機体に対してRigidbodyと機体の当たり判定の統合処理が行われ、非アクティブになっていた機体操作スクリプトをアクティブにしたり逆にクラフトスクリプトを非アクティブにしたりしてロボットを操作可能なプレイモードに移ります。プレイモードの時はEditボタンを押すとRigidbodyの削除やスクリプトの切り替え処理が行われエディットモードに戻ります。

 基本的にこのゲームの操作系は固定型です。細かい仕組みはあとで説明しますが例えロボットだろうと戦闘機だろうと宇宙戦艦だろうとあらゆる乗り物が同じ操作系で動かせるようにしています。

  • WASDキーで前後左右に平行移動
  • スペース&左シフトキーで上昇下降
  • 右クリック&マウスカーソルの移動でピッチ&ヨー
  • Q or Eキーでロール

 このような形でXYZ全軸の移動&回転操作を実現しています。

 最初はこれらの操作をすべてキーボードに割り振っていたのですが、あまりにも操作が大変で武器やその他機体操作と同時並行で動かしていたら指の本数が足りなくなることに気が付いたのでピッチとヨーをマウスカーソルの動きに連動させる形で負担を分散させることで誰でも思うがままに動かせる軽快な操作性を確保しました。

 ただし、ただロボットの形だけを作っただけでは動きません。宇宙で乗り物を動かすにはロケットエンジンが必要なのです。

 そのロケットエンジンにあたるのがスラスターです。

 このゲームではスラスターの物理演算は簡略化されています。スラスターの扱いは直接ロボットに推力を与えてくれるエンジンというよりロボットのスピードをアップさせるパワーアップアイテムの方が近いです。

 このゲームではフレーム更新がされるたびにスラスターのエネルギーと方向(推力ベクトル)をスクリプトによって一つ一つ分析しスラスターがどの移動や回転の時どれくらいのパワーで影響するのか調べた後合計し最終的に平行移動と回転運動を司る12個のPower変数に分割してまとめます。

 例えば正面方向に移動する力を発生させるスラスターのエネルギーはPowerForwardに加算され、ピッチ上方向に回転させるスラスターのエネルギーはPowerPitchUp に加算され……という形ですべてのスラスターの推力ベクトルを分析し分解する処理をフレーム更新のたびにやっています(この更新処理は理論上ロボットの構造に変化か生じたときだけ呼び出せばいいのであとで最適化しようと思っています)

 プログラム的にはロボットを動かしているのは計算によって導き出された12のPower変数の数値でありスラスターそのものではなく、スラスターはPower変数の数値を増減させる強化パーツに過ぎないという形です。

 なんでこんな変わった処理をしているのかというと、単にUnityの便利で高性能な物理演算をそのまま使うとロボットのような複雑な乗り物を宇宙で動かすとき不都合になるからです。

代替テキスト
推力や重心のバランスが狂うと回転モーメントが発生する



 例えば推力バランスや重心の問題です。乗り物のバランスが崩れると回転モーメントが発生し最悪自慢の作品がクルクルその場で回転し続けるだけの未確認飛行物体になってしまいます。普通の乗り物クラフト系ゲームではプレイヤーがこうしたバランス問題をあの手この手でねじ伏せて飛行機やロケットや宇宙船を飛ばすわけです。

 しかしロボットにとっては単なる調整だけでは済みません。

ロボットには手足やその他可動部品がありますよね?
手足や部品が動けば重心も推力バランスも変わってしまいますよね?



 これがロボットを飛行させるとき最大の障害となる要因です。パーツと重心と推力バランスが動くロボットがまっすぐ飛ぶよう調整したり手動でコントロールするのは不可能です。いいとこ不動の大仏を弾道飛行させるのが限界でしょう。宇宙空間では重力や空気抵抗のような複雑な物理演算は無視できますがそれでもバランスの問題だけは避けて通れません。

 例えば自動でバランスをとってくれるしくみがあればこの限りではありません。勝手に機体のバランスをとってくれるジャイロパーツ、あるいはオートバランサー。そうした機能をプレイヤーやゲーム側が用意してくれるなら複雑なロボットも空を自由自在に飛ばすことが出来るでしょう。

 しかし、このような自動化システムを扱うのはプレイヤーにとってもプログラマーにとっても、あるいはゲームを動かすハードウェアにとっても負担のかかる難しい方法です。また宇宙空間では「バランス制御の基準軸が存在しない」という問題もあります。

 舞台が地球なら空と大地をつなげるY軸を基準に、ワールドのY軸と機体のY軸が一致するようXZ軸の回転を制御する処理をすればいいわけです。しかし宇宙には空も大地もないですし、上も下もありません。バランスをとるときの基準にする基準軸がないためバランスのとりようがないわけです。

 一応無理やり宇宙空間に空と地上と基準軸を設定する方法も取れなくはないですが宇宙空間という舞台を用意した意味はほぼなくなります。私個人の感想ですが宇宙空間は上も下もない無重力空間だからいいのです。それをなくしてしまうくらいなら最初から地球を舞台にすればいいと思っています。

 そもそもです。私の作るゲームにそんな高度な処理は必要あるのでしょうか?リアルにこだわりすぎて作りたいものも作れなくなるくらいなら最初から複雑な処理を省いて可能な限り簡単な処理にまとめようというのが私の選んだ結論です(その結果プログラム的には一層複雑になってしまったのは皮肉ですが)

代替テキスト
回転モーメントを省き平行移動として扱う


代替テキスト
こんな形でも問題なくまっすぐ飛ぶ


 スラスターの推力をPower変数にまとめるときに回転モーメントの計算は省略します。だから推力バランスが崩れていても重心が悪くてもロボットは入力された方向に対して平行移動します。

 それだけではありません。たとえばスラスターの角度が斜め45度のような微妙な角度だった時はどうでしょうか。ロボットの軸とスラスターの軸と一致していないこのスラスターの推力ベクトルをそのまま利用するとロボットは斜めの方向に飛んで行ってしまいますね。じゃあ四捨五入の要領で角度の細かい部分は無視してXY軸のうち近いほうの軸と一致する形でベクトルを曲げるのはどうでしょうか。15度や37度みたいな微妙な角度のスラスターならそれでもいいでしょうが、今回のようなきっかり45度の角度のスラスターの扱いには困ってしまいますね。

代替テキスト
斜めのスラスターは別の処理が必要


 私のプログラムではこのような斜めのスラスターの推力ベクトルはスラスター側でVector3(X、Y、Zの三つの変数が一つにまとまった変数)に分解しています。例えば2次元XYの平面に斜め上45度に伸びるベクトルがあったとしたらそのベクトルはX成分とY成分に分解されるわけです。それを三次元で行っているのが私のスラスターです。

代替テキスト
片方の成分ベクトルを切り捨ててもう片方のベクトルを利用する



 そしてX軸方向に進むならX成分のベクトルを、Y軸方向に進むならY成分のベクトルを取得して利用するという処理を行っているわけです。

 つまり一つのスラスターで二つの方向に進むことが出来るという現実世界じゃ絶対にありえない動きをしているわけですがそこはいったんおいておいて……一つ重要なのは一つのベクトルを成分に分解している為斜めのスラスターのエネルギーは100%移動・回転に変換されるわけではないということです。

 たとえば45度に傾いたスラスターのベクトルの長さの比率は三平方の定理を用いて計算してみると1、1、√2になるのでこのスラスターはX、Y軸方向に移動するとき本来のスラスターの推力の約70%しかエネルギーを出せないことになります。のちのちエネルギー消費システムを追加してスラスター移動しすぎるとエネルギー切れで動けなる要素を追加しようと思っているのですがたとえ70%しか推力が利用できなくてもエネルギーの消費量は変わらない設定にするつもりなので斜めのスラスターが多いとエネルギーをそれだけ無駄にするわけです。

 簡単に言うと高度な物理シミュレーションに頭を悩ませる必要がなくなった代わりに「スラスターの推力をいかに効率よく乗り物に伝えエネルギーの消費を抑えるか」という新しいギミック要素が生まれたわけです。

 高度な物理演算システムは複雑な乗り物を作る上でははっきり言って天敵です。しかし単純に削除するだけでは今度は「誰でも簡単にスーパーマシンを生み出せてしまう」ということになりゲームバランスが崩壊します。そんなに簡単になんでも作れてしまったらプレイヤーはあっという間にゲームの要素を遊びつくして最後は飽きてしまうでしょう。だからそれに代わる要素を取り入れたいのです。それが私がやろうとしている「エネルギー分配管理要素」なわけです。高度な物理演算ゲームと誰でもスーパーマシンを作れるゲームの中間点を狙いたいのです。私はここに少なくない潜在ニーズがあると思っています。

まだ整理整頓が行き届いていない雑なコードですが、これらの動作は以下のスクリプトによって実行されます。

Thruster
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
using UnityEngine;

public class Thruster : MonoBehaviour
{
    // 推進力の大きさ
    [SerializeField] private float Power = 10f;

    // 計算されたベクトルを保持する
    [SerializeField] private Vector3 localThrustVector;    // ローカル座標系での推進ベクトル
    [SerializeField] private Vector3 localThrustX;         // X成分
    [SerializeField] private Vector3 localThrustY;         // Y成分
    [SerializeField] private Vector3 localThrustZ;         // Z成分

    // 白い線の表示切り替え
    [SerializeField] private bool A = true;

    // 親オブジェクトの参照
    private Transform parentTransform;

    void Start()
    {
        // 親オブジェクトを取得
        if (transform.parent != null)
        {
            parentTransform = transform.parent;
        }
        else
        {
            Debug.LogWarning("親オブジェクトが存在しません。ワールド座標基準で描画します。");
        }
    }

    void Update()
    {
        // ローカル座標系での推進ベクトルを計算 (Vector3.Down方向にPowerを掛ける)
        localThrustVector = transform.localRotation * Vector3.down * Power;

        // 親の回転を考慮してワールド座標系に変換
        Vector3 worldThrustVector = parentTransform != null
            ? parentTransform.rotation * localThrustVector
            : localThrustVector;

        // 成分ごとに分解 (ローカル座標系基準)
        localThrustX = new Vector3(localThrustVector.x, 0, 0);
        localThrustY = new Vector3(0, localThrustVector.y, 0);
        localThrustZ = new Vector3(0, 0, localThrustVector.z);

        // 可視化のための線を描画
        DrawDebugLines(worldThrustVector);
    }

    // 可視化用のデバッグ線を描画する
    void DrawDebugLines(Vector3 worldThrustVector)
    {
        // オブジェクトの位置を基点とする
        Vector3 origin = transform.position;

        // 親の回転を基準にしたローカル→ワールド座標変換
        Quaternion parentRotation = parentTransform != null ? parentTransform.rotation : Quaternion.identity;

        // ローカル座標での各軸成分を親の回転に合わせて描画
        Debug.DrawLine(origin, origin + parentRotation * localThrustX, Color.red);   // X軸成分 (赤)
        Debug.DrawLine(origin, origin + parentRotation * localThrustY, Color.green); // Y軸成分 (緑)
        Debug.DrawLine(origin, origin + parentRotation * localThrustZ, Color.blue);  // Z軸成分 (青)

        // 白い線は条件付きでワールド座標基準で表示
        if (A)
        {
            Debug.DrawLine(origin, origin + worldThrustVector, Color.white);
        }
    }

    // 各成分を取得するメソッド
    public Vector3 GetLocalThrustX() => localThrustX;
    public Vector3 GetLocalThrustY() => localThrustY;
    public Vector3 GetLocalThrustZ() => localThrustZ;

    // ローカル座標系での推進ベクトル全体を取得するメソッド
    public Vector3 GetLocalThrustVector() => localThrustVector;

}

RobotController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
using UnityEngine;
using UnityEngine.UIElements;

public class RobotController : MonoBehaviour
{
    [SerializeField] private float PowerForward = 0f;
    [SerializeField] private float PowerRight = 0f;
    [SerializeField] private float PowerLeft = 0f;
    [SerializeField] private float PowerBack = 0f;
    [SerializeField] private float PowerUp = 0f;     // 上昇用の推力
    [SerializeField] private float PowerDown = 0f;   // 下降用の推力

    // ピッチ上下エネルギー
    [SerializeField] private float PowerPitchUp = 0f;
    [SerializeField] private float PowerPitchDown = 0f;

    // ヨー左右エネルギー
    [SerializeField] private float PowerYawLeft = 0f;
    [SerializeField] private float PowerYawRight = 0f;

    // ロール左右エネルギー
    [SerializeField] private float PowerRollLeft = 0f;
    [SerializeField] private float PowerRollRight = 0f;

    [SerializeField] private float DampingFactor = 0.98f;       // 減衰率 (0.98 = 2%ずつ減少)
    [SerializeField] private float MinThreshold = 0.01f;        // 最小閾値 (これ以下はゼロとみなす)

    private Rigidbody rb;
    private ThrusterVectorVisualizer[] thrusters;

    // マウスの感度
    public float MouseSensitivity = 1f;

    // シリアライズされたプライベート変数として宣言
    [SerializeField] private float pitchInput = 0f; // ピッチ入力
    [SerializeField] private float yawInput = 0f;   // ヨー入力
    [SerializeField] private float RollInput = 0f;   // ロール入力

    void Start()
    {
        rb = GetComponent<Rigidbody>();
        // Rigidbodyの設定を調整
        rb.maxAngularVelocity = 100f;  // 最大角速度を設定
        rb.angularDrag = 0.05f;        // 回転の減衰を設定
    }

    void Update()
    {
        // 常に最新のスラスター取得
        thrusters = GetComponentsInChildren<ThrusterVectorVisualizer>();

        // 入力を取得
        //平行移動
        float moveForward = Input.GetKey(KeyCode.W) ? 1 : 0;
        float moveBack = Input.GetKey(KeyCode.S) ? 1 : 0;
        float moveRight = Input.GetKey(KeyCode.D) ? 1 : 0;
        float moveLeft = Input.GetKey(KeyCode.A) ? 1 : 0;
        float moveUp = Input.GetKey(KeyCode.Space) ? 1 : 0;       // 上昇 (スペースキー)
        float moveDown = Input.GetKey(KeyCode.LeftShift) ? 1 : 0;  // 下降 (シフトキー)

        //回転(ロールのみ)
        float RollLeft = Input.GetKey(KeyCode.Q) ? 1 : 0;       // 上昇 (スペースキー)
        float RollRight = Input.GetKey(KeyCode.E) ? 1 : 0;  // 下降 (シフトキー)

        // Power変数の初期化
        // 平行移動
        PowerForward = 0f;
        PowerBack = 0f;
        PowerRight = 0f;
        PowerLeft = 0f;
        PowerUp = 0f;
        PowerDown = 0f;
        // 回転
        PowerPitchUp = 0f;
        PowerPitchDown = 0f;
        PowerYawLeft = 0f;
        PowerYawRight = 0f;
        PowerRollLeft = 0f;
        PowerRollRight = 0f;

        // スラスターから推力を取得し、方向ごとに加算
        foreach (var thruster in thrusters)
        {
            Vector3 thrust = thruster.GetLocalThrustVector();  // 修正箇所
            PowerForward += thrust.z > 0 ? thrust.z : 0;
            PowerBack += thrust.z < 0 ? -thrust.z : 0;
            PowerRight += thrust.x > 0 ? thrust.x : 0;
            PowerLeft += thrust.x < 0 ? -thrust.x : 0;
            PowerUp += thrust.y > 0 ? thrust.y : 0;      // 上昇の推力
            PowerDown += thrust.y < 0 ? -thrust.y : 0;   // 下降の推力

            // ピッチ下エネルギーの計算
            if (thruster.transform.localPosition.z > 0 && thrust.y < 0) // Z+の位置にあり、Yが負
            {
                PowerPitchDown += Mathf.Abs(thrust.y);
            }
            else if (thruster.transform.localPosition.z < 0 && thrust.y > 0) // Z-の位置にあり、Yが正
            {
                PowerPitchDown += Mathf.Abs(thrust.y);
            }
            // ピッチ上エネルギーの計算
            if (thruster.transform.localPosition.z > 0 && thrust.y > 0) // Z+の位置にあり、Yが正
            {
                PowerPitchUp += Mathf.Abs(thrust.y);
            }
            else if (thruster.transform.localPosition.z < 0 && thrust.y < 0) // Z-の位置にあり、Yが負
            {
                PowerPitchUp += Mathf.Abs(thrust.y);
            }

            // ヨー左エネルギーの計算
            if (thruster.transform.localPosition.z > 0 && thrust.x < 0) // Z+の位置にあり、Xが負
            {
                PowerYawLeft += Mathf.Abs(thrust.x);
            }
            else if (thruster.transform.localPosition.z < 0 && thrust.x > 0) // Z-の位置にあり、Xが正
            {
                PowerYawLeft += Mathf.Abs(thrust.x);
            }

            // ヨー右エネルギーの計算
            if (thruster.transform.localPosition.z > 0 && thrust.x > 0) // Z+の位置にあり、Xが負
            {
                PowerYawRight += Mathf.Abs(thrust.x);
            }
            else if (thruster.transform.localPosition.z < 0 && thrust.x < 0) // Z-の位置にあり、Xが正
            {
                PowerYawRight += Mathf.Abs(thrust.x);
            }

            //ロール左エネルギーの計算
            if (thruster.transform.localPosition.x > 0 && thrust.y < 0) // X+の位置にあり、Yが負
            {
                PowerRollLeft += Mathf.Abs(thrust.y);
            }
            else if (thruster.transform.localPosition.x < 0 && thrust.y > 0) // X-の位置にあり、Yが正
            {
                PowerRollLeft += Mathf.Abs(thrust.y);
            }

            // ロール右エネルギーの計算
            if (thruster.transform.localPosition.x > 0 && thrust.y > 0) // Z+の位置にあり、Yが正
            {
                PowerRollRight += Mathf.Abs(thrust.y);
            }
            else if (thruster.transform.localPosition.x < 0 && thrust.y < 0) // Z-の位置にあり、Yが負
            {
                PowerRollRight += Mathf.Abs(thrust.y);
            }
        }

        // 入力があるときだけ推力を反映
        Vector3 force = new Vector3(
            (moveRight * PowerRight - moveLeft * PowerLeft),
            (moveUp * PowerUp - moveDown * PowerDown),   // 上昇・下降の推力
            (moveForward * PowerForward - moveBack * PowerBack)
        );

        rb.AddRelativeForce(force);

        // 入力がない場合のみ自然減衰を実行
        if (moveForward + moveBack + moveRight + moveLeft + moveUp + moveDown == 0)
        {
            ApplyNaturalDamping();
        }

        // マウス右ボタンを押している間のみ回転を実行
        if (Input.GetMouseButton(1)) // 右クリック
        {
            UnityEngine.Cursor.visible = false;  // カーソルを非表示
            UnityEngine.Cursor.lockState = CursorLockMode.Locked; // カーソルをロック(画面の中央に固定)

            if (Input.GetAxis("Mouse Y") > 0)
            {
                pitchInput = Input.GetAxis("Mouse Y") * MouseSensitivity * PowerPitchUp;
            }
            else if (Input.GetAxis("Mouse Y") < 0)
            {
                pitchInput = Input.GetAxis("Mouse Y") * MouseSensitivity * PowerPitchDown;
            }
            if (Input.GetAxis("Mouse X") > 0)
            {
                yawInput = Input.GetAxis("Mouse X") * MouseSensitivity * PowerYawLeft;
            }
            else if (Input.GetAxis("Mouse X") < 0)
            {
                yawInput = Input.GetAxis("Mouse X") * MouseSensitivity * PowerYawRight;
            }

            Vector3 worldTorqueX = transform.right * pitchInput;  // ローカルX軸
            Vector3 worldTorqueY = transform.up * yawInput;     // ローカルY軸

            // トルクベクトルを合成
            Vector3 totalTorque = -worldTorqueX + worldTorqueY;

            // Rigidbodyにローカル座標系で回転するトルクを加える
            rb.AddTorque(totalTorque, ForceMode.Force);

        }
        else
        {
            UnityEngine.Cursor.visible = true;  // カーソルを表示
            UnityEngine.Cursor.lockState = CursorLockMode.None; // カーソルをロック解除
        }

        //ロールの回転処理
        RollInput = RollLeft * PowerRollLeft - RollRight * PowerRollRight;
        Vector3 worldTorqueZ = transform.forward * RollInput; // ローカルZ軸
        rb.AddTorque(worldTorqueZ, ForceMode.Force);

        //// デバッグ用にピッチ上下エネルギー、ヨー左右エネルギーを表示
        //Debug.Log("Pitch Up Energy: " + PowerPitchUp);
        //Debug.Log("Pitch Down Energy: " + PowerPitchDown);
        //Debug.Log("Yaw Left Energy: " + PowerYawLeft);
        //Debug.Log("Yaw Right Energy: " + PowerYawRight);
    }

    // 自然減衰処理
    void ApplyNaturalDamping()
    {
        // 速度の自然減衰
        rb.velocity *= DampingFactor;
        if (rb.velocity.magnitude < MinThreshold)
        {
            rb.velocity = Vector3.zero;
        }

        // 回転の自然減衰
        rb.angularVelocity *= DampingFactor;
        if (rb.angularVelocity.magnitude < MinThreshold)
        {
            rb.angularVelocity = Vector3.zero;
        }
    }
    private void OnEnable()
    {
        rb = GetComponent<Rigidbody>();
    }
}


今回のゲーム制作ではchatGPTのコーディング能力と先人たちが作ったCreasiegeというゲームデータを利用しています。

CreasiegeのURL

 このデータは2019年に配布されたスペースシップクラフトゲームのデータです。
 Besiegeという有名なクラフトゲームを参考に宇宙を舞台に宇宙船を作ってミッションをこなすという内容で、最低限ではありますがタイトルやメニュー画面、クラフト、制御、基本的な要素は一通り備えています。説明文を見た感じフランスの専門学校の生徒が作成したものらしく私個人としては親近感が湧きますね。

 ただし、自分の掲げる「極力ゲーム内の仕組みを単純に、それでいて好きなものを好きなように作れるクラフトゲームを作る」という目標とは真逆の物理演算を正確にシミュレートするタイプのゲームだったのでコードをそのまま流用するのは技術的にも方向性の違い的にも難しいです。そのため自分のゲームのすべてのコードはAIの力を借りながら独力で制作しました。Creasiegeを制作したフランスの専門学校の生徒は時期的にもAIのサポートを受けずにほぼ独力でゲームを制作していたと思うので自分もそれくらいできるようになりたいですね。

今後の課題

 この記事を書く少し前までは前述のスラスターの処理が最大の課題だったのですが、それをchatGPTの力も借りながらなんとかひと段落させたので現在の課題はUIとシステム周りの整備になっています。

 中でも直近の課題はデータベースの整理です。実は前述の機能以外にもJsonファイルを用いた簡易的なセーブ&ロードの機能を実現するスクリプトも実装していたのですが、クラフトやスラスター関係のシステムの整備に時間を割きたかったこと、そしてなによりゲーム内で使用するデータを一括管理するデータベースを整備していなかったことからいったん排除していました。

 例えばデータベースがない今のままではブロックのデータはクラフトスクリプトとセーブ&ロードスクリプトで別々に設定しなければなりません。他にもブロックのデータを必要とするスクリプトがあればそのたびにいちいちリスト型変数にブロックのプレハブデータを格納していかなければならないのです。このようなデータ関係を一括で管理するデータベースオブジェクトをシーン内に設置して、他のスクリプトはそこからデータをもらって利用する形にすれば今後の機能追加もグッと楽になります。

 それが終わればUI周りの整備です。リスト型変数にブロックデータを格納するだけで勝手にボタン配置まで完了させるスクリプトを書けば後が楽になりますね。

 あとは既存のコードをオブジェクト指向的に整理整頓していくことも必要です。例えばクラフト関係を処理するスクリプトはenum型とswitch文、メソッドによる処理の分割等によってだいぶわかりやすく整理整頓したのですが、先ほど掲載したRobotControllerスクリプトのようにまだ整理しきれていないスクリプトもあります。

 最初はオブジェクト指向やスクリプトの整理についてあまり考えず行き当たりばったりにスクリプトを書いていたのですが、あまりにも複雑になりすぎて自分の理解の範疇を超えてしまい挫折しかけたことがありました。しかし一度コードを大幅に削って各処理を分割してメソッドにまとめてスクリプトを整理していくことで同じ処理をメソッドとして使いまわせるようにしたりSwitch文でモードごとの処理の切り替えをわかりやすくしたりしてなんとかここまでこぎつけたわけです。いままでイメージがあやふやだったオブジェクト指向という概念が自分の中で一気に明確化された気がします。

 次回の記事ではより一層進化したスペースロボクラをお披露目できるよう頑張ります。