恐縮ですが、テレビ東京放送(BSテレ東)制作の2025年春 まで放送された「東京パソコンクラブ〜プログラミング女子のゼロからゲーム作り〜」のunityのパートについての考察をはじめました。非公式であります。
放送当時のUnityのバージョンと現在普及しているバージョンの違いがあって、多少放送時のプログラムソースの記述が異なるものも登場することがあります。
第48回21分43秒とか時間の紹介がありますが、放送をご覧になる環境によって多少ズレるので、参考程度にお聞きください。
(4-010)
テレビ番組「東京パソコンクラブ」で紹介されたUnityによるシューティングゲーム制作の過程を、非公式に補足・解説したものです。番組放送時と現在のソフトウェアバージョンの違いを踏まえ、Unityを使用した2Dレーシングゲーム制作の工程を解説します。車両のアクセル、ブレーキ、ハンドリングといった挙動のプログラミングから、コースの構築やカメラの追従設定までが段階的に説明されています。特に当たり判定にはBoxCastを用いた処理が施されています。また、スタート信号の演出や、ラップタイムを正確に計測してリザルトを表示する実装についても触れています。

1.まずは前進まで
①初期設定、(第63回19分51秒)
Free Aspect>Half HD(960×540)
Main Camera>Camera>Sizeのところ、5を270に設定(最終バージョンは20)
一旦Ctrl+Sでセーブ。シーン名はRace
②「Car」の位置を動かす変数を入力(第63回20分48秒)
あらかじめCarの画像を用意しておく。まあ、最近はGoogleGemini、ナノバナナも無課金でも賢くなったので、いい時代ですね。すぐにできました。
それで、小林先生は、今回使用する画像やコンポーネントをAssetsの中にEASYという名前のフォルダに全部つっこんでますね。
Carの画像をHierarchyにドロップする。
New Script(C#Script) の名前をCarにする。つまりCar.csの作成にとりかかる。
自分の位置を動かすためにTransform型の変数、名前は何でもいいんだけど、本放送にあわせて、今回はtfという名前で作る。
public Transform tf;
③加速度・ブレーキ・ハンドルの変数を入力(第63回21分37秒)
他の変数も作る。アクセル、つまり加速度用の変数の宣言
public float accel;
他の変数も同様に作っていく。初期値は0であるとして、
public float accel = 0; //加速力
public float handle = 0; //曲がる力
public float brake = 0; //ブレーキ
④速度の変数を入力(第63回22分7秒)
float v = 0;
publicを付けていないのでprivateだね。
publicを付けた変数はCar.csの外部からもアクセスできるようになる。でも今回vは何もつけていないので、速度vの変数はCar.csの内部だけで使用する変数、ということだな。
⑤「上矢印を押したら前へ進む」プログラム(第63回22分15秒)
次に上矢印キーで前進する、というのを試しに作る、ということで。前回「シューティング」ではっきりしてきたGetkeyは古い命令だという話もありまして、
・・・・
・・・こんなかんじに
if文の中、
v += accel * Time.deltaTime;
Time.deltaTimeは1フレーム間の処理にばらつきがあっても、大体同じくらいの速度になるプログラムです。
⑥速度が増えた分移動する処理(第63回23分13秒)
tf.localPosition += tf.up * v * Time.deltaTime;
ここで注意すべきなのがupというところ。今までは空間での上下左右を基準に考えていたが、今回のupは、あくまでも車両の前進方向としてupを考えるので、車両がヨコをむいていたら、その方向がupとなる。そうなると画面上でupの方向は上ではなくなるな。あくまでも車のフロントだ。
じゃあ、変数名をフロントでいいような気もするが、本編ではupになっていた。
それではaccelに500を入れて前進させてみると・・・
・・・前進できています。
第63回の前半はここまで。
2.ハンドルの完成
⑦アクセルを踏まないと自動的に遅くなる処理(第63回23分58秒)
v *= brake;
brakeの数値は0.98
⑧バックの処理(第63回24分21秒)
先程の前進する処理をコピーして、下にバック用として張り付ける。
書き換える(上を下に。+=を-=に)
動作の確認
⑨ハンドルを左側にきる処理(第63回24分57秒)
先程の前進する処理をコピーして、下にハンドル用として張り付ける。
書き換える(上を左に)
if文の中の処理については、
回転についてはZ軸の角度を変更させればよいので、
tf.Rotate(0, 0, v * handle * Time.deltaTime);
vを式に加えて掛け算している意味は、v=0、つまり停止中は角度が変わらない、とするため。逆に速度が大きいときは角度も大きく変わる、とするためです。
右ハンドルについては全体にマイナスを掛ける、まあ、vの前にマイナスを掛け算すればよいので、こうなります。
次の図は第63回26分7秒で放送された図で、座標と車体の傾きを表しています。(モーション解説の回のYowも参考になります。)

動作の確認
3.コースとカメラの設定
①コースを作る(第64回14分6秒)
「コースはあらかじめ作ってある」という小林先生のお言葉。グサッときますねえ。
それで、こちらもあらかじめ作らないといけないということで・・・・
・・・どうしましょうかねえ。
第64回14分16秒に全体のコースが見えています。
・・・ええと、Courseというオブジェクトが、何やら親子関係もあるブラックボックスみたいになっていまして、
それでもコース作りにとりかからないと、先進めませんからねえ。
それで、始める前に、小林先生がドーンとコースをひとまとめでオブジェクトにしていたじゃないですか。あれ、よく考えてみると1枚の絵ではなくて、Wallのプレハブの集団ともいえる訳です。ちょっとAIのCopilotさんに聞いてみると、
Course (Prefab)
├ Wall
├ Wall
├ Wall
└ …(大量)
という 1 つの Prefab が完成します。
ということで、前回、シューティングでプレハブを作る際には「親子関係を作るべきではない」と言いましたが、それは子の中の値が固定化してしまうので、子の値が変更できるプレハブを作る時は、親子関係を解消して、一つのオブジェクトにまとめてからプレハブを作るとうまくいくよ、という話であって、今回はプログラム上でプレハブ化したコースを湧き出させるような演出として使う訳ではなく、別のコースを作るにしても、ゲーム上、「ドーン」と配置させるものであるので、こういう場合は、こういう親子関係で管理してもかまわない、ってことですね。本当にややこしい話ですが。
それで、ここで話が脱線してしまうようですが、みなさんに実際に放送で使われた本編のゲームの方で遊んでみましょう。UnityRoomで・・・。
・・・まあ、なかなかすばらしいですね。これに似たものの完成を目指す、ということになりますが、さてコースをどうするか?そのままマネして問題ないか?とかうp主さんは考えましたが、結論として、逆送コースをやってみよう!ということにしました。これが第64回14分16秒のシーンに登場する本編のコースです。これを180度回転させると逆送コースになりますね。
・・・まあ、それで、Course (Prefab)の中身については先に本編の後半部分を参照して以下の様にわかりました。
Course
Wall(Wallの子は壁のスクリプトか?)←おそらくこれはプレハブ化して大量に存在している。
Road(Roadの子は路のスクリプトか?)←おそらくこれは最低1枚でもよい。だけどデザインてきには時計回りに誘導する感じで4枚かな?
Line(Lineの子は境界線のスクリプトか?)←これはちょっとわからない。たぶん無くても
大丈夫?棚上げ。
Light
Circle
Circle(1)
Circle(2)
Circle(3) 0~3は消灯:暗めの絵柄を最初に有効にしておく。
Circle(4)
Circle(5)
Circle(6)
Circle(7) 4~7は点灯:明るめの絵柄を最初に無効にしておく。
・・・・それで後はひたすら完成を目指す、ということです。ちなみにRoadはレイヤーの設定で、車両や壁は0のままで、Roadは-2にすることにより(GoalやCheckPointを-1にするため)、車両よりRoadは奥に描かれるようにして、Road自体は当たり判定とは関係ない様にしています。
②車をカメラで追わせる(第64回14分32秒)
今回使用する画像やコンポーネントのフォルダ、車の画像も含めてEASYフォルダ、ここに小林先生は、さらにFollowCamera.csも用意してある、ということで、これでブラックボックスが2つになりました。まあ、なんとなくこっちの方の中身は、車を追いかけるカメラであるので、従来の知識で想像できますが。
第64回14分43秒で、MainCameraを選択したままで、FollowCamera.csをAddコンポーネントする。

↑FollowCamera.cs
↑MainCameraのインスペクターに、このFollowCamera.csを(Add Compornent、ドラッグアンドドロップで)加えます。
これで、壁は貫通するが、カメラは車を追うようになります。本編では第64回14分59秒で弓木さんの車が暴走するというアクシデントが発生していました。
第64回の前半はここでおしまい。
4.当たり判定とタイムなど→完成
荒っぽい表示なのでカメラのサイズを本編と違うサイズに変える。(サイズ270→20)
Lightsを配置する。
③車の「当たり判定」をプログラム(第64回15分44秒)
3つめのブラックボックス、Car.csです・・・いや、さっきまでCar.csを作っていたよね?本編ではあらかじめ用意していた当たり判定付きのCar.csなんですよ。
まあ、うp主は、今のところ車両の操作の命令だけをこの動画で作ってきたので、当たり判定の部分はブラックボックスになっている訳です。
第64回15分55秒で、一部が見えるのですが・・・Raycastを使っていますね。
ええと、ここで確認できるのは(最終的には利用しませんのでコード表示にしていません)、当たり判定にbool型のisHit、
Vector2 origin = _t.position -_t.right * 16;
Vector2 direction = _t.up;
float distance =32;
Debug.DrawRay(origin , direction * distance, Color.red);
RaycastHit2D hit = Psysics2D.Raycast(origin, direction , distance);
if (hit.collider != null && !hit.collider.Trigger)
{
Vector3 diff = origin + direction * distance -hit.point;
_t.localPosition -= diff;
・・・・
この当たり判定は、車が壁に重なったら「当たった」と判断し、重ならない状態まで押し戻すプログラムだそうで・・・うーん、ここで思ったのが、ちょうどRaycastが初めて登場したスパマリ風のプログラム(このシリーズの第6回)・・・あれをそのまま応用した方が早いんじゃないか?と思ったので、急遽あちらのプログラムから転用して・・・
・・・最終的にはRaycastでなくてBoxCastの方が確実に壁との当たり判定が有効にはたらくんだけどね。(RayCast設定時には壁のすりぬけとかが発生)
BoxCastにより、当たり判定が安定になりました。
BoxCastにより、当たり判定が安定になったが、念のためコース外になったときに、画面外(960×540より外)になったときに原点(X0,Y0)に戻る様に変更しました。Wall(1)を無効にしてコース外にでて原点の戻るかを試します。
試験走行しているうちに、車体の後方の色を赤から黄色にした方が、車体の前方が現在どこなのかを把握しやすくなるため、黄色に塗りました。
④タイム測定づくり(第64回19分14秒)
タイムの制御、つまりゲーム全体を指揮するためのプログラムとして新たにRaceManagerというオブジェクトと、その中にRaceManager.csを作成します。
ゴールとチェックポイントの作成
ゴールとチェックポイントにBoxCollider2Dを加えて(動画では間違えてRigidbody2Dを加えているので連携できずに失敗している)作成 RaceManagerにアタッチする。
第64回21分35秒に、インスペクターの様子がみえます。

CanvasからタイムやLAPのテキスト追加、Result(最終結果)用のオブジェクトの追加
第64回21分13秒から、for部分などの追加。追加箇所は紫色。
public GameObject result;
public txResult;
public Car car;
public BoxCollider2D checkpoint;
public BoxCollider2D goal;
float[] rapTime = new float[4]; //4周する。0から3まで計4回記録する。
// Start is called before the first frame update
IEnumerator Start()
{
rapTime[0] = Time.time; // rapTimeは走り出した瞬間からゴールを通るまでの時間
for(int i=0; i<3;i++)
{
while(BoxCollider2D.OverlapPoint(car._t.positon) != goal)
{
yield return null;
}
rapTime[i+1] = Time.time; - raptime[i]
}
result.SetActive(true);
txResult.text += “LAP1” + ConvertTimeString(rapTime[1] - rapTime[0]) + “\n”;
txResult.text += “LAP2” + ConvertTimeString(rapTime[2] - rapTime[1]) + “\n”;
txResult.text += “LAP3” + ConvertTimeString(rapTime[3] - rapTime[2]);+ “\n”
txResult.text += “------------------------\n”;
txResult.text += “TIME” + ConvertTimeString(rapTime[3] - rapTime[0]);
car.enabled = false;
txTime.gameObject.SetActive(false);
}
// Update is called once per frame
void Update()
{
・・・・・
第64回22分4秒からの追加。スタート時のシグナルの演出の追加。追加箇所は、紫色。
public GameObject result;
public txResult;
public Car car;
public BoxCollider2D checkpoint;
public BoxCollider2D goal;
float[] rapTime = new float[4]; //4周する。0から3まで計4回記録する。
// Start is called before the first frame update
//---------------------------------------
//処理フロー
//---------------------------------------
IEnumerator Start()
{
rapTime[0] = Time.time; // rapTimeは走り出した瞬間からゴールを通るまでの時間
car.enabled = false;
lights[0].SetActive(true);
yield return new WaitForSeconds(1);
lights[1].SetActive(true);
yield return new WaitForSeconds(1);
lights[2].SetActive(true);
yield return new WaitForSeconds(1);
lights[3].SetActive(true);
for(int i=0; i<3;i++)
{
while(BoxCollider2D.OverlapPoint(car._t.positon) != goal)
{
yield return null;
}
rapTime[i+1] = Time.time; - raptime[i]
}
result.SetActive(true)
txResult.text += “LAP1” + ConvertTimeString(rapTime[1] - rapTime[0]) + “\n”;
txResult.text += “LAP2” + ConvertTimeString(rapTime[2] - rapTime[1]) + “\n”;
txResult.text += “LAP3” + ConvertTimeString(rapTime[3] - rapTime[2]);+ “\n”
txResult.text += “------------------------\n”;
txResult.text += “TIME” + ConvertTimeString(rapTime[3] - rapTime[0]);
car.enabled = false;
txTime.gameObject.SetActive(false);
}
// Update is called once per frame
void Update()
{
…
第64回22分9秒でCourseの構造が少しわかります。
Course
Wall(Wallの子は壁のスクリプトか?)
Road(Roadの子は路のスクリプトか?)
Line(Lineの子は境界線のスクリプトか?)
Light
Circle
Circle(1)
Circle(2)
Circle(3) 0~3は消灯:暗めの絵柄を最初に有効にしておく。
Circle(4)
Circle(5)
Circle(6)
Circle(7) 4~7は点灯:明るめの絵柄を最初に無効にしておく。
Check
Square
Square(1)
RaceManager
Canvas
Timer (最初は無効)
Result (最初は無効)
Image
Timer
TimerとかCanvasの空間なので
Canvasのカメラの変更「Screen Space Overlay」または「Screen Space – Camera」←これは当初からなっている。
これで完成? 確認がいちいち3周するのが大変なので
第64回22分55秒で、小林先生は「ゴールの横の壁のBoxColliderのチェックを一旦外して無効にすれば、楽に3周できます」ということで、それに従ってまずは確認の作業を行う。
Resultの大きさ修正など確認できたら、ゴールの横の壁のBoxColliderのチェックを戻して完成。
スタート位置をチェッカーに変更 → 完成
今回のソースとインスペクター
(01)FollowCamera.cs

(02)MainCameraのインスペクター

(03)Car.cs

(04)Carのインスペクター

(05)Coraceの子Wallの子Wall(1)のインスペクター

(06)Coraceの子Roadの子Road(1)のインスペクター

(07)Coraceの子Lightsの子Circle(7)のインスペクター

(08)Coraceの子Checkの子Squareのインスペクター

(09)Coraceの子Checkの子Squre(1)のインスペクター

(10)Coraceの子Checkの子Squre(1)のインスペクター(Color)

(11)RaceManager.cs

(12)RaceManagerのインスペクター

(13)Canvasのインスペクター

(14)Canvasの子Timerインスペクター(1)

(15)Canvasの子Timerインスペクター(2)

(16)Canvasの子txLapインスペクター(1)

(17)Canvasの子txLapインスペクター(2)

(18)Canvasの子Resultインスペクター

(19)Canvasの子Resultの子txResultインスペクター(1)

(20)Canvasの子Resultの子txResultインスペクター(2)

袋とじ:自分の作ったゲームを配布してみよう
①フォルダの作成
まずはAssetsフォルダの外にBuildフォルダを作成、
その中に
自分が今作成したいゲームのフォルダを作成することになります。
Build Profilesから Scene List→今作成したシーンを加える。(ほかのものはチェックをはずす)
ビルドの時のシーンの登録時に順番が大事で、上に登録したシーンから起動する。(しろさと情報)
パターンセッティングスの設定は以下のものがあるが、基本的にデフォルトでよい
②ゲーム内容紹介欄の作成
プレイヤーセッティングスの設定
Company Name 作者の名前→(c) hirogreat.com/
Product Name 作ったゲームの名前→なんとかGAME
Iconも設定できる(Default Icon)
③ビルドして配布するぞ!
BuildボタンとBuild And Runボタン→「作成」と「作成後実行する」ボタン
先程作成した「ビルドフォルダ」を選ぶ。選んだらBuildが開始される。Build作成時には、数分かかり、Unity側で操作ができなくなるので注意が必要。
実行ウインドウ。画面の大きさ、画面のクオリティ、モニターの選択などが選べる。ウィンドウのチェックをはずすとフルスクリーンになる。
作成されたビルドのファイルの説明(一例)

全画面表示の時に終了する時はAlt+F4キーで終了。
配布する時は、この「ビルド」全体を配布してください。
フォルダの中身の名前を変更すると実行できなくなるので、そのまま配布してください。
補足として、エンディングで紹介したスイッチオンラボさんのWeb上で発表する方法の動画を紹介しておきます。
【詳細解説】Unityで作ったゲームをWebで公開する方法!【UnityPlay / unityroom / 自分のWebサイト】
配布(Windows用 ダウンロード)
今回のゲーム「レーシング」をBuildにより作成して、配布してみたいと思います。
今回の「レーシング」のダウンロードサイト

今回の「レーシング」のダウンロードはこちら(Windows用)


コメント