Delphi で学ぶ古典ゲームの仕組み 第４回：入力を受け付けよう

# はじめに

前回、Update と Render を使ってボールをランダムに動かす事ができました。
今回は、キー入力に応じてボールを打つようにしてみます。

# ゲームパッド
今回作っている物はゲームコンソールではなく普通の PC で動作するためキーボードで操作することを前提にしています。

とはいえ、せっかくなので仮想的なゲームパッドを想定しておきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/7751c188-bbf8-d01a-dcab-d57d383a207c.png" width=400>

スーファミのコントローラーをモチーフにしつつ [start] [select] ボタンを無くしました。

このゲームパッドを示すクラスを作ります。

## コード

クラスの宣言はこのような感じです。

```delphi
type
  // ボタンの種類
  TPadButton = (Left, Up, Right, Down, A, B, X, Y, L, R);

  // ゲームパッドを表す型
  TGamePad = record
  private type
    TButtonInfo = record
      FIsPressed: Boolean;
      FHoldTimeWatch: TStopwatch;
      FHoldTime: Integer;
    end;
  private var
    FButtons: array [TPadButton] of TButtonInfo;
  public
    // ボタンが押されたときに呼ぶ
    procedure Press(const AButton: TPadButton);
    // ボタンが離されたときに呼ぶ
    procedure Release(const AButton: TPadButton);
    // ボタンが押されているか確認する
    function IsPressed(const AButton: TPadButton): Boolean;
    // ボタンが押されていた時間 [msec]
    function GetHoldTime(const AButton: TPadButton): Integer;
  end;
```

実装部は↓の通りで、単純に各ボタンが押されたときに FIsPressed を True にしたり、押されていた時間を計測しているだけです。

```delphi
function TGamePad.GetHoldTime(const AButton: TPadButton): Integer;
begin
  Result := FButtons[AButton].FHoldTime;
end;

function TGamePad.IsPressed(const AButton: TPadButton): Boolean;
begin
  Result := FButtons[AButton].FIsPressed;
end;

procedure TGamePad.Press(const AButton: TPadButton);
begin
  with FButtons[AButton] do
  begin
    if FIsPressed then
      FHoldTime := FHoldTimeWatch.ElapsedMilliseconds
    else
    begin
      FIsPressed := True;
      FHoldTime := 0;

      FHoldTimeWatch.Reset;
      FHoldTimeWatch.Start;
    end;
  end;
end;

procedure TGamePad.Release(const AButton: TPadButton);
begin
  with FButtons[AButton] do
  begin
    FHoldTimeWatch.Stop;

    FIsPressed := False;
    FHoldTime := FHoldTimeWatch.ElapsedMilliseconds;
  end;
end;
```

:::note warn 押されていた時間について
GamePad は人間の入力を扱うためゲームのフレームレート外で動作します。
そのため、押されていた時間をフレームレートと連動して測ることはできません。
たとえば、自機を移動するといった時も 100[msec] でどのぐらい移動するのか、といった別の時間軸を必要とします。
:::

## GameMaster で管理する

TGamePad のインスタンスを GameMaster のプロパティとして公開して、どこからでも参照できるようにします。

```delphi
  TGameMaster = class
  (略)
  private var
    (略)
    FPad: TGamePad;
  (略)
  public
    property Pad: TGamePad read FPad;
```

# キーボードとゲームパッドを結びつける

ゲームパッドをキーボードと関連付けます。
具体的には以下のキーが押された時、ゲームパッドのボタンが押されたことにします。
※「未割り当て」はキーを割り当てていません

|ゲームパッドのボタン|キーボード|
|---|---|
|Up|↑|
|Down|↓|
|Left|←|
|Right|→|
|L|未割り当て|
|R|未割り当て|
|X|未割り当て|
|Y|未割り当て|
|A|Space|
|B|Shift|

:::note info
ハードウェアと仮想的なゲームパッドを結びつける方法は、メリットが１つあります。
それは、操作のカスタマイズです。
例えば、下記のようにスペースキーを押した時 A ボタンの機能が発動されるようになっていた時
```スペースキー → A ボタン```
これを下記のように変更すると
```スペースキー → B ボタン```
スペースキーを押した時に B ボタンの機能が発動されるようにできるのです。
これを利用してキーとボタンの関係をいくつか定義しておけば、ユーザー好みの設定で遊べるようになります（もちろん全てのキーを自由にカスタマイズできてもかまいません）
:::

それでは、実際にキーボードでキーが押された時、GamePad のボタンが押された処理を呼ぶ様にします。
そのためには TForm の OnKeyDown / OnkeyUp を使います。
各キーが押されたタイミングで ```press``` を呼び、離したタイミングで ```release``` を呼んでいます。 

```delphi
procedure TfrmMain.FormKeyDown(
  Sender: TObject;
  var Key: Word;
  var KeyChar: WideChar;
  Shift: TShiftState);
begin
  // キーボードのキーが押されたらゲームパッドのボタンを押す
  case Key of
    vkLeft:  FGameMaster.Pad.Press(TPadButton.Left);
    vkRight: FGameMaster.Pad.Press(TPadButton.Right);
    vkUp:    FGameMaster.Pad.Press(TPadButton.Up);
    vkDown:  FGameMaster.Pad.Press(TPadButton.Down);
    vkShift: FGameMaster.Pad.Press(TPadButton.B);
  end;

  case KeyChar of
    ' ': FGameMaster.Pad.Press(TPadButton.A);
  end;
end;

procedure TfrmMain.FormKeyUp(
  Sender: TObject;
  var Key: Word;
  var KeyChar: WideChar;
  Shift: TShiftState);
begin
  // キーボードのキーが離されたらゲームパッドのボタンを離す
  case Key of
    vkLeft:  FGameMaster.Pad.Release(TPadButton.Left);
    vkRight: FGameMaster.Pad.Release(TPadButton.Right);
    vkUp:    FGameMaster.Pad.Release(TPadButton.Up);
    vkDown:  FGameMaster.Pad.Release(TPadButton.Down);
    vkShift: FGameMaster.Pad.Release(TPadButton.B);
  end;

  case KeyChar of
    ' ': FGameMaster.Pad.Release(TPadButton.A);
  end;
end;
```

# ボールを発射する

では、A ボタンが押されたときにボールを発射するようにして見ます。
TGameMaster の TLoopThread 内に下記の様に書いておきます。
（TBallObject は初期位置などを後から設定出来るように改修されています）

```delphi
  while (not Terminated) and (not Application.Terminated) do
  begin
    （略）

    // 入力検知
    if FGameMaster.Pad.IsPressed(TPadButton.A) then
    begin
      // ボールを生成
      var B := TBallObject.Create(FGameMaster);
      B.SetParam(
        TBallColor(Random(3)), // ボールの色
        100,                   // ボールの X 座標
        100,                   // ボールの Y 座標
        50,                    // ボールの直径
        Random(10) + 1,        // ボールの X 方向の速度
        Random(10) + 1         // ボールの Y 方向の速度
      );
    end;
```

実行してスペースキーを押してみると…

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/110d3dc2-85d4-45b6-a1be-8533387355e4.gif" width=300>

１回キーを押しただけなのに、大量にボールが生成されました。
これは、LoopThread 内部は常に回っているため IsPressed で A ボタンが押されている間は、ボールが生成され続けるためです。
そのため、ボールが１つ生成されたら Release されるまでボールを生成しないようにする必要があります。

## ボールの生成を制御する

ボールの生成を制御するために、新たに TBallLauncher クラスを作成します。
このクラスは Press → Release という遷移を取った時だけボールを生成します。
つまり、１回押すと１個ボールが生成される仕組みです。

```delphi:TBallLauncherクラスの宣言
type
  TBallLauncher = class(TGameObject)
  private var
    FCanSpawnBall: Boolean; // ボールを生成可能か示すフラグ
  public
    procedure Start; override;
    procedure Update; override;
  end;
```

先ほど GameMaster に書いたボールの生成処理をこちらに移動します。

```delphi:TBallLauncherクラスの実装
procedure TBallLauncher.Start;
begin
  FCanSpawnBall := True; // 初期化時ボールを生成可能にします
end;

procedure TBallLauncher.Update;
begin
  // A ボタンが押されているとき
  if GameMaster.Pad.IsPressed(TPadButton.A) then
  begin
    // ボールが生成不可のときはそのまま抜けます
    if not FCanSpawnBall then
      Exit;

    FCanSpawnBall := False;

    // ボールを生成
    var B := TBallObject.Create(GameMaster);
    B.SetParam(
      TBallColor(Random(3)),
      100,
      100,
      50,
      Random(10) + 1,
      Random(10) + 1
    );
  end
  else
  begin
    // A ボタンが離された場合は生成可能にします
    FCanSpawnBall := True;
  end;
end;

end.
```

:::note info
TBallLauncher のように実体を持たないものにも GameObject は利用できます。
:::

押しっぱなしにしても１つしか生成されなくなりました！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/4d21aa2f-2693-b039-309f-40fa7971b7b0.gif" width=300>

# 砲台を作りボールを飛ばす

TBallLauncher を拡張して、砲台を作ります。
(ちょっと上の note で実体が無くても利用できますと書いたのに実体を作ります…)

砲台なので、A ボタンを押した時、ボールを打ち出します。
A ボタンを押す度に下記の状態を上から順々に進めていき、最後に発射します。

|状態|動作|
|---|---|
|Stop|ボタンが押されたら Prepare に移動|
|Prepare|Color に遷移する前の準備|
|Color|色選択：色が 16 フレーム毎に変わります|
|Inflate|大きさ選択：大きさが変わります|
|Angle|角度選択：射出線が出るので適切な角度を選びます|
|Speed|速度選択：射出線の長さが長ければ長いほど速くなります|
|Launch|発射！|

以下、これを実装した物です。
(アニメーション GIF にしたことで動きがガクガクになっていますが、もっと滑らかに動きます)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/6c8e57b4-5298-867a-17bb-7f3974fd5681.gif" width=300>

大分ゲームらしくなりましたね！

記事が長くなってしまったため、動作についてはソースコードを見てください。
基本は今までと同じで Update で計算をして Render で描画しています。

<details><summary>uGameMaster コード全文</summary>

```delphi
unit uGameMaster;

interface

uses
  System.Classes
  , System.Diagnostics
  , System.Types
  , System.SysUtils
  , System.Generics.Collections
  , FMX.Graphics
  , uGamePad
  ;

type
  TGameObject = class;

  TGameMaster = class
  private type
    TLoopThread = class(TThread)
    private var
      FFPS: Double;
      FFrameCount: Integer;
      FFPSWatch: TStopWatch;
      FGameMaster: TGameMaster;
    private
      procedure Update;
      procedure Render;
    protected
      procedure Execute; override;
    public
      constructor Create(const AGM: TGameMaster); reintroduce;
    end;
  private var
    FLoop: TLoopThread;
    FCanvas: TCanvas;
    FCanvasR: TRectF;
    FObjects: TList<TGameObject>;
    FPad: TGamePad;
    FBallLauncher: TGameObject;
  private
    procedure CalcCanvasRect;
  public
    constructor Create(const ACanvas: TCanvas); reintroduce;
    destructor Destroy; override;
  public
    procedure Start;
    procedure AddObject(const AObj: TGameObject);
    procedure RemoveObject(const AObj: TGameObject);
  public
    property Canvas: TCanvas read FCanvas;
    property CanvasR: TRectF read FCanvasR;
    property Pad: TGamePad read FPad;
    property BallLauncher: TGameObject read FBallLauncher;
  end;

  TGameObject = class
  private var
    FGameMaster: TGameMaster;
    FX: Single;
    FY: Single;
    FOldX: Single;
    FOldY: Single;
    FWidth: Single;
    FHeight: Single;
  private
    function GetDX: Single;
    function GetDY: Single;
  protected
    property GameMaster: TGameMaster read FGameMaster;
  public
    constructor Create(const AGM: TGameMaster); reintroduce; virtual;
    destructor Destroy; override;
    procedure Move(const AX, AY: Single);
    procedure Start; virtual;
    procedure Update; virtual;
    procedure Render; virtual;
    procedure Finish; virtual;
  public
    property X: Single read FX write FX;
    property Y: Single read FY write FY;
    property OldX: Single read FOldX write FOldX;
    property OldY: Single read FOldY write FOldY;
    property Width: Single read FWidth write FWidth;
    property Height: Single read FHeight write FHeight;
    property DX: Single read GetDX;
    property DY: Single read GetDY;
  end;

implementation

uses
  System.UITypes
  , System.Math
  , FMX.Forms
  , FMX.Types
  , uBallLauncher
  ;

type
  TStopwatchHelper = record helper for TStopwatch
  private
    function GetElapsedMicroSec: Double;
  public
    property ElapsedMicroSec: Double read GetElapsedMicroSec;
  end;

{ TStopwatchHelper }

function TStopwatchHelper.GetElapsedMicroSec: Double;
begin
  Result := Elapsed.Ticks * 1000 / TStopwatch.Frequency;
end;

{ TGameMaster }

procedure TGameMaster.AddObject(const AObj: TGameObject);
begin
  FObjects.Add(AObj);
end;

procedure TGameMaster.CalcCanvasRect;
begin
  FCanvasR := RectF(0, 0, FCanvas.Width, FCanvas.Height);
end;

constructor TGameMaster.Create(const ACanvas: TCanvas);
begin
  inherited Create;

  FObjects := TList<TGameObject>.Create;
  FBallLauncher := TBallLauncher.Create(Self); // GameObject なので自動的に破棄

  FCanvas := ACanvas;
  CalcCanvasRect;

  FLoop := TLoopThread.Create(Self);
end;

destructor TGameMaster.Destroy;
begin
  FLoop.Terminate;
  while not FLoop.Finished do
    Sleep(100);

  var Objects := FObjects;
  for var Obj in Objects do
    Obj.Free;

  FObjects.Free;

  inherited;
end;

procedure TGameMaster.RemoveObject(const AObj: TGameObject);
begin
  FObjects.Remove(AObj);
end;

procedure TGameMaster.Start;
begin
  if not FLoop.Started then
    FLoop.Start;
end;

{ TGameLoop.TLoopThread }

constructor TGameMaster.TLoopThread.Create(const AGM: TGameMaster);
begin
  inherited Create(True);

  FGameMaster := AGM;
  FreeOnTerminate := True;
end;

procedure TGameMaster.TLoopThread.Execute;
begin
  var SW: TStopwatch;
  var SleepWatch: TStopwatch;
  var AdjustTime := 0.0;

  FFrameCount := 0;
  FFPSWatch := TStopwatch.StartNew;

  // アプリケーションやスレッドが終了したら終わらせる
  while (not Terminated) and (not Application.Terminated) do
  begin
    SW.Reset;
    SW.Start;

    // 描画矩形の計算
    FGameMaster.CalcCanvasRect;

    // 更新・描画処理を呼び出す
    Update;
    Render;

    SW.Stop;

    // 余り時間
    var SleepTime := 16.67 - SW.ElapsedMicroSec - AdjustTime;

    if SleepTime > 0 then
    begin
      SleepWatch.Reset;
      SleepWatch.Start;

      Sleep(Round(SleepTime));

      SleepWatch.Stop;

      AdjustTime := SleepWatch.ElapsedMicroSec - SleepTime;
    end
    else
      AdjustTime := 0;
  end;

  // 終了処理
end;

procedure TGameMaster.TLoopThread.Render;
begin
  FGameMaster.FCanvas.BeginScene;
  try
    try
      // 背景のクリア
      FGameMaster.FCanvas.Clear(TAlphaColors.Black);

      // 各描画処理を呼ぶ
      for var Obj in FGameMaster.FObjects do
         Obj.Render;

      // FPS 表示
      FGameMaster.FCanvas.Fill.Color := TAlphaColors.White;
      FGameMaster.FCanvas.Fill.Kind := TBrushKind.Solid;
      FGameMaster.FCanvas.FillText(
        RectF(
          6,
          FGameMaster.FCanvasR.Bottom - 28,
          FGameMaster.FCanvas.Width,
          FGameMaster.FCanvasR.Bottom
        ),
        Format('FPS: %.3f', [FFPS]),
        False,
        1,
        [],
        TTextAlign.Leading,
        TTextAlign.Center);
    except
    end;
  finally
    FGameMaster.FCanvas.EndScene;;
  end;

  // FPS 更新処理
  Inc(FFrameCount);
  var FPSElapsed := FFPSWatch.ElapsedMilliseconds;
  if FPSElapsed > 999 then
  begin
    FFPS := FFrameCount * 1000 /  FPSElapsed;

    FFrameCount := 0;
    FFPSWatch.Reset;
    FFPSWatch.Start;
  end;
end;

procedure TGameMaster.TLoopThread.Update;
begin
  // 各更新処理を呼ぶ
  for var Obj in FGameMaster.FObjects do
     Obj.Update;
end;

{ TGameObject }

constructor TGameObject.Create(const AGM: TGameMaster);
begin
  inherited Create;

  FGameMaster := AGM;
  FGameMaster.AddObject(Self);

  Start;
end;

destructor TGameObject.Destroy;
begin
  Finish;

  FGameMaster.RemoveObject(Self);

  inherited;
end;

procedure TGameObject.Finish;
begin
  // 継承先で変更
end;

function TGameObject.GetDX: Single;
begin
  Result := FX - FOldX;
end;

function TGameObject.GetDY: Single;
begin
  Result := FY - FOldY;
end;

procedure TGameObject.Move(const AX, AY: Single);
begin
  FOldX := FX;
  FOldY := FY;

  if (FWidth = 0) or (FHeight = 0) then
  begin
    FX := AX;
    FY := AY;
  end
  else
  begin
    FX := EnsureRange(AX, 0, FGameMaster.CanvasR.Width - FWidth);
    FY := EnsureRange(AY, 0, FGameMaster.CanvasR.Height - FHeight);;
  end;
end;

procedure TGameObject.Render;
begin
  // 継承先で変更
end;

procedure TGameObject.Start;
begin
  // 継承先で変更
end;

procedure TGameObject.Update;
begin
  // 継承先で変更
end;

end.
```
</details>

<details><summary>uBallObject コード全文</summary>

```delphi
unit uBallObject;

interface

uses
  System.SysUtils
  , System.Types
  , System.UITypes
  , uGameMaster
  , PK.Utils.Log
  ;

type
  TBallColor = (Red, Green, Blue);

  TBallObject = class(TGameObject)
  private var
    FVX: Single;
    FVY: Single;
    FColor: TAlphaColor;
    FDiameter: Single;
    FVisible: Boolean;
  public
    procedure SetParam(
      const AColor: TBallColor;
      const AX, AY, ADiameter, AVX, AVY: Single);
    procedure Start; override;
    procedure Render; override;
    procedure Update; override;
    procedure Assign(const ASource: TBallObject);
    procedure ChangeSpeed(const AVX, AVY: Single);
    procedure ChangeColor(const AColor: TBallColor);
    property Color: TAlphaColor read FColor;
    property Visible: Boolean read FVisible write FVisible;
  end;

implementation

const
  BALL_COLOR_VALUES: array [TBallColor] of TAlphaColor = (
    TAlphaColors.Deeppink,
    TAlphaColors.Greenyellow,
    TAlphaColors.Dodgerblue
  );

{ TBallObject }

procedure TBallObject.Assign(const ASource: TBallObject);
begin
  FColor := ASource.FColor;
  X := ASource.X;
  Y := ASource.Y;
  FDiameter := ASource.FDiameter;
  FVX := ASource.FVX;
  FVY := ASource.FVY;
end;

procedure TBallObject.ChangeColor(const AColor: TBallColor);
begin
  FColor := BALL_COLOR_VALUES[AColor];
end;

procedure TBallObject.ChangeSpeed(const AVX, AVY: Single);
begin
  FVX := AVX;
  FVY := AVY;
end;

procedure TBallObject.Render;
begin
  if FVisible then
  begin
    GameMaster.Canvas.Fill.Color := FColor;
    GameMaster.Canvas.FillEllipse(RectF(X, Y, X + FDiameter, Y + FDiameter), 1);
  end;
end;

procedure TBallObject.SetParam(
  const AColor: TBallColor;
  const AX, AY, ADiameter, AVX, AVY: Single);
begin
  FColor := BALL_COLOR_VALUES[AColor];
  X := AX;
  Y := AY;
  FDiameter := ADiameter;
  FVX := AVX;
  FVY := AVY;
end;

procedure TBallObject.Start;
begin
  FVisible := True;
end;

procedure TBallObject.Update;
begin
  var R := GameMaster.CanvasR;
  R.Right := R.Right - FDiameter;
  R.Bottom := R.Bottom - FDiameter;

  if (X < 0) or (X > R.Width) then
    FVX := -FVX;

  if (Y < 0) or (Y > R.Height) then
    FVY := -FVY;

  Move(X + FVX, Y + FVY);
end;

end.
```
</details>

<details><summary>uBallLauncher コード全文</summary>

```delphi
unit uBallLauncher;

interface

uses
  System.SysUtils
  , uGameMaster
  , uGamePad
  , uBallObject
  ;

type
  TBallState = (Stop, Prepare, Color, Inflate, Angle, Speed, Launch);

  TBallLauncher = class(TGameObject)
  private const
    BALL_DIAMETER_MAX = 50;
    POD_WIDTH = 80;
    POD_HEIGHT = 8;
    ANGLE_MIN = 15;
    ANGLE_MAX = 165;
    SPEED_MIN = 3;
    SPEED_MAX = 24;
    SPEED_K = 5;
    COLOR_WAIT = 16;
    SELF_WIDTH = 80;
    SELF_HEIGHT = 8;
    SELF_SPEED = 8;
  private var
    FBall: TBallObject;
    FBallState: TBallState;
    FBallColor: TBallColor;
    FBallAngle: Single;
    FBallSpeed: Single;
    FBallDiameter: Single;
    FSign: Integer;
    FColorWait: Integer;
    FPressed: Boolean;
  public
    procedure Start; override;
    procedure Update; override;
    procedure Render; override;
  end;

implementation

uses
  System.Types
  , System.UITypes
  , System.Math
  , System.Math.Vectors
  , FMX.Graphics
  , PK.Utils.Log
  ;

{ TBallLauncher }

procedure TBallLauncher.Render;
begin
  with GameMaster, Canvas do
  begin
    Fill.Kind := TBrushKind.Solid;
    Fill.Color := TAlphaColors.White;

    FillRect(RectF(X, Y, X + POD_WIDTH, Y + POD_HEIGHT), 1);

    case FBallState of
      TBallState.Angle, TBallState.Speed:
      begin
        Stroke.Kind := TBrushKind.Solid;
        Stroke.Dash := TStrokeDash.Dash;
        Stroke.Thickness := 1;
        Stroke.Color := FBall.Color;

        var OX := FBall.X + FBallDiameter / 2;
        var OY := FBall.Y + FBallDiameter / 2;

        var C, S: Single;
        SinCos(DegToRad(FBallAngle), S, C);

        var  Sp := FBallSpeed * SPEED_K;

        DrawLine(PointF(OX, OY), PointF(OX + Sp * C, OY - Sp * S), 1);
      end;
    end;
  end;
end;

procedure TBallLauncher.Start;
begin
  FBall := TBallObject.Create(GameMaster);
  FBall.Visible := False;

  Width := SELF_WIDTH;
  Height := SELF_HEIGHT;
end;

procedure TBallLauncher.Update;

  procedure IncValue(var AValue: Single; const AMin, AMax: Single);
  begin
    AValue := AValue + FSign;

    if (AValue < AMin) then
    begin
      AValue := AMin;
      FSign := -FSign;
    end;

    if (AValue > AMax) then
    begin
      AValue := AMax;
      FSign := -FSign;
    end;
  end;

begin
  if Y = 0 then
  begin
    // 初期化: 位置を中央下部に
    Move(
      (GameMaster.CanvasR.Width - Width) / 2,
      GameMaster.CanvasR.Height - Height);
  end;

  if GameMaster.Pad.IsPressed(TPadButton.Left) then
  begin
    // 砲台を動かす
    Move(X - SELF_SPEED, Y);
  end;

  if GameMaster.Pad.IsPressed(TPadButton.Right) then
  begin
    // 砲台を動かす
    Move(X + SELF_SPEED, Y);
  end;

  if GameMaster.Pad.IsPressed(TPadButton.A) then
  begin
    // A ボタンが押されたら
    FPressed := True;

    case FBallState of
      TBallState.Stop:
      begin
        // 準備に移動
        FBallState := TBallState.Prepare;
      end;
    end;
  end
  else
  begin
    // A ボタンが離されていたら
    case FBallState of
      TBallState.Prepare:
      begin
        // ボール準備
        if FPressed then
        begin
          FBallState := TBallState.Color;
          FBallColor := TBallColor.Red;
          FBallDiameter := BALL_DIAMETER_MAX;
          FColorWait := 0;
          FBall.Visible := True;
        end;
      end;

      // 色選択
      TBallState.Color:
      begin
        if FPressed then
        begin
          FBallState := Inflate;
          FSign := -1;
        end;

        Inc(FColorWait);
        if FColorWait = COLOR_WAIT then
        begin
          FColorWait := 0;

          var C := Ord(FBallColor) + 1;
          if (C > Ord(High(TBallColor))) then
            FBallColor := Low(TBallColor)
          else
            Inc(FBallColor);

          FBall.ChangeColor(FBallColor);
        end;
      end;

      // サイズ選択
      TBallState.Inflate:
      begin
        if FPressed then
        begin
          FBallState := Angle;
          FSign := 2;
          FBallSpeed := SPEED_MAX;
        end;

        IncValue(FBallDiameter, 0, BALL_DIAMETER_MAX);
      end;

      // 角度選択
      TBallState.Angle:
      begin
        if FPressed then
        begin
          FBallState := Speed;
          FSign := 1;
        end;

        IncValue(FBallAngle, ANGLE_MIN, ANGLE_MAX);
      end;

      // 速度選択
      TBallState.Speed:
      begin
        if FPressed then
          FBallState := TBallState.Launch;

        IncValue(FBallSpeed, SPEED_MIN, SPEED_MAX);
      end;

      // ボールを発射
      TBallState.Launch:
      begin
        var B := TBallObject.Create(GameMaster);
        B.Assign(FBall);
        var C, S: Single;
        SinCos(DegToRad(FBallAngle), S, C);
        B.ChangeSpeed(C * FBallSpeed, -S * FBallSpeed);

        FBall.Visible := False;
        FBallState := TBallState.Stop;
      end;
    end;

    FPressed := False;

    // 状態を反映
    FBall.SetParam(
      FBallColor,
      X + (Width - FBallDiameter) / 2,
      Y - FBallDiameter, FBallDiameter,
      0,
      0);
  end;
end;

end.
```
</details>

# まとめ
今回、ユーザー入力を扱う方法を紹介しました。
是非、キー押して色々変わるようにしてみてください！

[第５回につづく](https://qiita.com/pik/items/529812e304eb7db974e5)