4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-04-26

はじめに

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

ゲームパッド

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

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

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

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

コード

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

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 にしたり、押されていた時間を計測しているだけです。

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;

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

GameMaster で管理する

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

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

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

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

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

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

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

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 は初期位置などを後から設定出来るように改修されています)

  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;

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

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

ボールの生成を制御する

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

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

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

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.

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

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

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

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

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

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

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

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

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

uGameMaster コード全文
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.
uBallObject コード全文
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.
uBallLauncher コード全文
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.

まとめ

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

第5回につづく

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?