はじめに
前回でゲームループが完成したので、動くキャラクターを作っていきます。
オブジェクト
ゲームで動く物を Star Force を例に考えるてみると…
- 自機
- 敵
- 弾
- 爆発
- アイテム
- 地上の構造物
- 陸地
ざっとこのような物が存在する事がわかります。
しかもどれも刻々と位置を変えたりアニメーションが変わったりします。
つまり、位置やアニメーションのカレントフレームなどの「状態」を持っています。
これらの物は「状態を持ったオブジェクト」ということになります。
状態を持ったオブジェクト
ゲーム中に表示されるほとんどの物が状態を持っている事がわかりました。
ということは、状態を更新するための Update メソッドを持たせないといけません。
ということで、これら状態を持ったオブジェクトのベースクラス TGameObject を作ります。
uGameMaster に新たに TGameObject を定義します。
TGameObject は Start, Finish, Update, Render を用意します。
また多くのオブジェクトが座標を必要としているため座標を保持する X, Y と前回の座標値 OldX, OldY, その差分 DX, DY というプロパティと Move というメソッドを定義します。
type
TGameObject = class
private var
FGameMaster: TGameMaster;
FX: Single;
FY: Single;
FOldX: Single;
FOldY: Single;
private
function GetDX: Single;
function GetDY: Single;
protected
procedure Start; virtual;
procedure Update; virtual;
procedure Render; virtual;
procedure Finish; virtual;
protected
property GameMaster: TGameMaster read FGameMaster;
public
constructor Create(const AGM: TGameMaster); reintroduce;
destructor Destroy; override;
procedure Move(const AX, AY: Single);
public
property X: Single read FX write FY;
property Y: Single read FY write FY;
property OldX: Single read FOldX write FOldX;
property OldY: Single read FOldY write FOldY;
property DX: Single read GetDX;
property DY: Single read GetDY;
end;
次に実装です。
ここでは生成時に GameMaster に自分を追加する処理を書いています。
(今はまだ機能がありませんが)AddObject で追加されたオブジェクトが GameMaster の Render / Update から呼ばれる想定です。
implementation
{ TGameObject }
constructor TGameObject.Create(const AGM: TGameMaster);
begin
inherited Create;
FGameMaster := AGM;
FGameMaster.AddObject(Self); // GameMaster に自身を追加
Start; // 継承先オブジェクトで初期化の機会を与える
end;
destructor TGameObject.Destroy;
begin
Finish; // 継承先オブジェクトで終了処理の機械を与える
FGameMaster.RemoveObject(Self); // GameMaster から自身を削除
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;
FX := AX;
FY := AY;
end;
procedure TGameObject.Render;
begin
// 継承先で変更
end;
procedure TGameObject.Start;
begin
// 継承先で変更
end;
procedure TGameObject.Update;
begin
// 継承先で変更
end;
end.
オブジェクトの管理
次に TGameMaster 側の処理を変更します。
TGameObject の管理用 FObjects を追加します。
FObjects に GameObject を追加するためのメソッド AddObject / RemoveObject を追加します。
TGameMaster = class
(略)
private var
(略)
FObjects: TList<TGameObject>; // これを追加
public
constructor Create(const ACanvas: TCanvas); reintroduce;
destructor Destroy; override; // これも追加
public
// この2つを追加
procedure AddObject(const AObj: TGameObject);
procedure RemoveObject(const AObj: TGameObject);
public
// 外部から描画できるように Canvas / Canvas R をプロパティとして公開
property Canvas: TCanvas read FCanvas;
property CanvasR: TRectF read FCanvasR;
end;
コンストラクタで FObjects を生成してデストラクタで廃棄しています。
AddObject で FObjects に GameObject を追加し、RemoveObject で GameObject を削除しています。
procedure TGameMaster.AddObject(const AObj: TGameObject);
begin
// 追加
FObjects.Add(AObj);
end;
constructor TGameMaster.Create(const ACanvas: TCanvas);
begin
inherited Create;
// 生成
FObjects := TList<TGameObject>.Create;
FCanvas := ACanvas;
FLoop := TLoopThread.Create(Self);
end;
destructor TGameMaster.Destroy;
begin
FLoop.Terminate;
while not FLoop.Finished do
Sleep(100);
// 廃棄
FObjects.Free;
inherited;
end;
procedure TGameMaster.RemoveObject(const AObj: TGameObject);
begin
// 削除
FObjects.Remove(AObj);
end;
オブジェクトの更新と描画
TLoopThread の Update / Render から各 GameObject の Render / Update を呼ぶ様に変更します。
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 表示
(略)
except
end;
finally
FGameMaster.FCanvas.EndScene;;
end;
// FPS 更新処理
(略)
end;
procedure TGameMaster.TLoopThread.Update;
begin
// 各更新処理を呼ぶ
for var Obj in FGameMaster.FObjects do
Obj.Update;
end;
これで準備が整いました。
オブジェクトを移動させる
ためしに、簡単なオブジェクトを作ってみましょう。
円を表示し移動して、画面の端まで来たら跳ね返える物を作ります。
※GIF アニメにキャプチャしているので滑らかではありませんが、実際はもっとずっと滑らかに動きます。
BallObject の作成
図の円を示すオブジェクトを TBallObject とします。
そして、TBallObject を記述するユニットを uBallObject とします。
まずは宣言部です。
unit uBallObject;
interface
uses
System.SysUtils
, System.Types
, System.UITypes
, uGameMaster
;
type
TBallObject = class(TGameObject)
private var
FVX: Single; // X速度 [pixel / frame]
FVY: Single; // Y速度 [pixel / frame]
FBoundsRect: TRectF; // 円の外接四角形
FColor: TAlphaColor; // 円の色
protected
procedure Start; override;
procedure Render; override;
procedure Update; override;
end;
Start,Render,Update を override して実際の処理を書いていきます。
実装部です。
procedure TBallObject.Render;
begin
// 円を描画する
GameMaster.Canvas.Fill.Color := FColor;
GameMaster.Canvas.FillEllipse(
RectF(X, Y, X + FBoundsRect.Width, Y + FBoundsRect.Height),
1
);
end;
procedure TBallObject.Start;
begin
// 初期設定
// 速度はランダム
FVX := Random(10) + 1;
FVY := Random(10) + 1;
// 大きさもランダム
var S := Random(30) + 5;
FBoundsRect := RectF(0, 0, S, S);
// 色もランダム
FColor := $ff_00_00_00 or UInt32(Random($1_00_00_00));
// 初期位置もランダム
var R := GameMaster.CanvasR;
Move(Random(Trunc(R.Width)), Random(Trunc(R.Height)));
end;
procedure TBallObject.Update;
begin
// Canvas外に出た時は符号を反転する
var R := GameMaster.CanvasR;
R.Right := R.Right - FBoundsRect.Width;
R.Bottom := R.Bottom - FBoundsRect.Height;
if (X < 0) or (X > R.Width) then
FVX := -FVX;
if (Y < 0) or (Y > R.Height) then
FVY := -FVY;
// FVX, FVY で表される速度で進んだ時の位置を計算
Move(X + FVX, Y + FVY);
end;
次に実際に TBallObject を生成する処理です。
これは単なるサンプルのため Main フォーム側に書きましょう。
procedure TfrmMain.FormCreate(Sender: TObject);
begin
FGameMaster := TGameMaster.Create(Canvas);
// TBallObject を生成
for var i := 0 to 100 do
TBallObject.Create(FGameMaster);
end;
ちなみに、ここでは 100 個生成していますがうちの環境(Core i7 14700K, RTX 4060Ti, Win64 Release Build)では 5000 個程度まで 60 [FPS] で動作しました。10000 個だと 45 [FPS]ぐらいでした。
TGameObject を継承して Update, Render など適切なメソッドを override するだけで簡単に動くオブジェクトを実装できることが理解できたかと思います。
ここまでのコード
uGameMaster と uBallObject のソースコード全文です。
uGameMaster コード全文
unit uGameMaster;
interface
uses
System.Classes
, System.Diagnostics
, System.Types
, System.SysUtils
, System.Generics.Collections
, FMX.Graphics
;
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>;
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;
end;
TGameObject = class
private var
FGameMaster: TGameMaster;
FX: Single;
FY: Single;
FOldX: Single;
FOldY: Single;
private
function GetDX: Single;
function GetDY: Single;
protected
procedure Start; virtual;
procedure Update; virtual;
procedure Render; virtual;
procedure Finish; virtual;
protected
property GameMaster: TGameMaster read FGameMaster;
public
constructor Create(const AGM: TGameMaster); reintroduce;
destructor Destroy; override;
procedure Move(const AX, AY: Single);
public
property X: Single read FX write FY;
property Y: Single read FY write FY;
property OldX: Single read FOldX write FOldX;
property OldY: Single read FOldY write FOldY;
property DX: Single read GetDX;
property DY: Single read GetDY;
end;
implementation
uses
System.UITypes
, FMX.Forms
, FMX.Types
;
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;
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;
FX := AX;
FY := AY;
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
;
type
TBallObject = class(TGameObject)
private var
FVX: Single;
FVY: Single;
FBoundsRect: TRectF;
FColor: TAlphaColor;
protected
procedure Start; override;
procedure Render; override;
procedure Update; override;
end;
implementation
{ TBallObject }
procedure TBallObject.Render;
begin
GameMaster.Canvas.Fill.Color := FColor;
GameMaster.Canvas.FillEllipse(
RectF(X, Y, X + FBoundsRect.Width, Y + FBoundsRect.Height),
1)
;
end;
procedure TBallObject.Start;
begin
FVX := Random(10) + 1;
FVY := Random(10) + 1;
var S := Random(30) + 5;
FBoundsRect := RectF(0, 0, S, S);
FColor := $ff_00_00_00 or UInt32(Random($1_00_00_00));
var R := GameMaster.CanvasR;
Move(Random(Trunc(R.Width)), Random(Trunc(R.Height)));
end;
procedure TBallObject.Update;
begin
var R := GameMaster.CanvasR;
R.Right := R.Right - FBoundsRect.Width;
R.Bottom := R.Bottom - FBoundsRect.Height;
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.
まとめ
自機、敵キャラ、など最初に上げた状態を持ったオブジェクトを TGameObject として制御する仕組みを紹介しました。
良かったら色々動かして試してみてください。