SwipeCar:止まったときの得点を出そう
この課題でやること
教科書のサンプルでは、スワイプで車を動かして、旗までの距離を画面に表示するところまででした。
この課題では、さらに次をプログラムで表現します。
- 車がだいたい止まったときだけ、距離に応じた得点を表示する
- 旗を越えて止まったら 0 点にする
ポイントは、車だけのスクリプトと、全体の進行を見るスクリプトに役割を分けることです。
CarController と GameDirector の役割
CarController(車側)
車には CarController を付けます。次のことを担当します。
- マウスでスワイプした長さから、進む速さ(
speed)を決める - 毎フレーム、車を動かす(
Translate)。だんだん遅くする(speed *= 0.98f) - 速さがほとんど 0 なら、きっちり 0 にそろえる(「止まった」と判定しやすくするため)
- ほかのスクリプトから見えるように、
speedと「一度でも走り始めたか」のisStartを public にする
GameDirector は、ここで公開した値を読むだけにすると整理しやすいです。
GameDirector(進行役)
GameDirector は、シーン全体の「進行役」です。
GameObject.Findで、車・旗・テキストを取得する(教科書と同じ考え方)- 距離
lengthを計算して、distanceの文字を更新する - 止まったあとだけ、
resultに得点を表示する
距離は、このプロジェクトでは次のように計算しています(GameDirector.cs)。
float length = this.flag.transform.position.x - this.car.transform.position.x;
this.distance.GetComponent<TextMeshProUGUI>().text = "Distance:" + length.ToString("F2") + "m";
x が大きいほど右だとすると、次のように読めます。
- 車が旗より左(まだ手前)→
lengthは 0 より大きい - 車と旗が同じ x →
lengthは 0 - 車が旗より右(旗を越えた)→
lengthは 0 より小さい(マイナス)
だから、「旗を越えた」は length < 0 で判定できます。
「止まった」とは何?
speed は毎フレームちょっとずつ小さくなりますが、コンピュータの計算では 本当に 0 にならないことがあります。
そこで CarController では、十分遅くなったら speed を 0 に固定します。
if (Mathf.Abs(this.speed) < 0.001f)
this.speed = 0f;
GameDirector 側でも、「止まった」とみなす条件を 同じくらいの小ささ(Mathf.Abs(cc.speed) < 0.001f)にそろえると、「まだ動いているのに得点が出た」というズレを防げます。
また isStart は、まだ一度もスワイプしていないときに得点が出ないようにするための変数です。ゲーム開始直後は車は止まっていますが、ここでは得点にしたくない、という意味です。
旗を越えたら 0 点にする(式だけだと困る理由)
得点は、距離から次のような 線形の式で求める例があります。
lengthが 0(旗のところ)に近いほど高得点- ある距離(この例では
14.5を基準)まで離れると 0 点に近づく
ここで注意なのは、length がマイナス(旗の右側)のときです。
length < 0 のまま、同じ式だけを使うと、計算が 意図しない大きさになり、「旗を越えたのに高得点」というおかしな結果になることがあります。
だから、先に length < 0 かどうかを分岐して、旗を越えて止まったら 0 Point と表示する、という書き方にします。
UI の書き方(教科書どおり)
TextMeshPro は、教科書では GameObject で名前を探して、GetComponent<TextMeshProUGUI>() で文字を変える書き方になっていることが多いです。このプロジェクトでも、distance と result は GameObject で持ち、表示するときに GetComponent しています。
(余裕が出たら、GetComponent を一度だけ取っておく書き方も覚えるとよいです。まずは教科書と同じ形で大丈夫です。)
図で整理する(この課題向け)
1. 位置関係と length の正負(数直線イメージ)
x は右ほど大きいとしたときのイメージです。

length = flag.x - car.x なので、車が旗より右にいるとマイナスになります。
2. 役割分担(だれが何を担当するか)

(車側では Translate などで毎フレーム動かしています。)
「車は動き」「進行役は距離と得点」が一目で分かります。
3. 得点を出すかどうか(フローチャート)

先に length < 0 を分ける理由は、「式だけだと旗の右側でおかしい点数になる」ためです。
4. 1 フレームの流れ(ざっくりシーケンス)
Unity は 毎フレーム Update が呼ばれます。両方のスクリプトがあるので、ざっくり次のような流れです(どちらの Update が先かは、プロジェクトの「スクリプト実行順」で変わることがあります)。

「メッセージを送っている」のではなく、同じフレーム内でそれぞれの Update が動くイメージです。
うまくいかないときのチェックリスト
- Hierarchy の名前が、
car_0/flag_0/distance/resultとコードのFindと同じか - 得点用の
resultという名前の Text をシーンに置いたか - 旗を越えた判定は
length < 0で合っているか(シーンで車と旗の左右を確認) 14.5は、教材のステージの幅に合わせた数字か(違うと満点の感覚がずれる)
つまずいたら:こう進めよう
全部を一度に書くと大変なので、次のように段階を分けると取り組みやすいです。
- 距離表示まで(教科書どおり)—
lengthと TextMeshPro が動くか確認 - 止まったときだけ得点—
speedを public にする、止まった判定、resultを追加 - 旗を越えたら 0 点—
length < 0のときは"0 Point"、それ以外は式で得点
次のようなところは、教科書にそのまま載っていないことが多いので、ヒントを見ながらでよいです。
- 止まった判定 — 小数の誤差があるので、
0.001fのような閾値を使う。車とGameDirectorで同じ数字にする isStart— 「まだプレイしていないのに得点が出る」のを防ぐための変数- 得点の式 — 何で割っているか(
14.5)が、どの距離で何点かに対応しているか、言葉で説明できると理解が深まります
まとめ
- 車(CarController) は、動きと「止まったら速度 0」まで担当する。
- 進行役(GameDirector) は、距離を見て、止まったあと・走り始めたあとだけ得点を出す。
- 旗を越えたは
length < 0。得点の式だけではマイナスのときにおかしくなるので、先に分岐する。
using UnityEngine;
using UnityEngine.InputSystem; // 入力を検知するために必要!!
public class CarController : MonoBehaviour
{
public float speed = 0;
public bool isStart = false;
Vector2 startPos;
void Start()
{
Application.targetFrameRate = 60;
}
void Update()
{
// スワイプの長さを求める
if (Mouse.current.leftButton.wasPressedThisFrame)
{
// マウスをクリックした座標
this.startPos = Mouse.current.position.value;
}
else if (Mouse.current.leftButton.wasReleasedThisFrame)
{
// マウスを離した座標
Vector2 endPos = Mouse.current.position.value;
float swipeLength = endPos.x - this.startPos.x;
// スワイプの長さを初速度に変換する
this.speed = swipeLength / 500.0f;
// 画面上のスワイプ量(ピクセル)で開始判定(速度の閾値より任せやすい)
if (Mathf.Abs(swipeLength) > 0.1f)
this.isStart = true;
// 効果音再生
GetComponent<AudioSource>().Play();
}
transform.Translate(this.speed, 0, 0); // 移動
this.speed *= 0.98f; // 減速
// 十分遅くなったら 0 にそろえる(止まった判定を確実にする)
if (Mathf.Abs(this.speed) < 0.001f)
this.speed = 0f;
}
}
using TMPro; // TextMeshProを使うために必要!
using UnityEngine;
public class GameDirector : MonoBehaviour
{
GameObject car;
GameObject flag;
GameObject distance;
GameObject result;
void Start()
{
this.car = GameObject.Find("car_0");
this.flag = GameObject.Find("flag_0");
this.distance = GameObject.Find("distance");
this.result = GameObject.Find("result");
}
void Update()
{
CarController cc = this.car.GetComponent<CarController>();
float length = this.flag.transform.position.x - this.car.transform.position.x;
this.distance.GetComponent<TextMeshProUGUI>().text = "Distance:" + length.ToString("F2") + "m";
if (cc.isStart && Mathf.Abs(cc.speed) < 0.001f)
{
if (length < 0f)
this.result.GetComponent<TextMeshProUGUI>().text = "0 Point";
else
{
int pts = (int)Mathf.Max(0f, (14.5f - length) * 100f / 14.5f);
this.result.GetComponent<TextMeshProUGUI>().text = pts + " Point";
}
}
}
}
ここまでできると、「なぜスクリプトを分けるのか」が自分の言葉で説明しやすくなります。余裕があれば(将来)、得点を一度だけ表示する、リトライボタンを付けるなど、ゲームとして広げてみてください。





ディスカッション
コメント一覧
まだ、コメントがありません