游戏开发入门"/>
Pascal游戏开发入门
1. 概览
前言
乱弹
常见的游戏开发有c/c++(Unreal), C#(Unity)等, Pascal语言的也有(),但是和前者对比不够流行。
关于pascal的优势,网上都说时易于教学,可以培养良好的程序习惯云云,我只是听之而已。
如果说最后需要c++,为什么一开始就面对呢,非要用pascal绕一圈,得不偿失的。
以上仅是个人观点
为什么有这系列文章
在Pascal基础系列文章第一篇我曾写到闲的无聊,学习一下pascal
, 目前也是如此。
如果为了学习游戏开发,快速上手自然要用Unreal或者Unity。 从基础做起,估计要学习图形学之类的知识。
但是我是闲情偶记(记录的记)
本系列关注于2D Code,不关注游戏性(关卡设计等)以及游戏相关资源(字体,美术,音乐音效等)的创建
环境
图形库选择
Pascal是跨平台的,如果要写的程序也要跨平台,可能需要使用OpenGL(相比Vulkan可以支持更多的旧设备)好一些。
但是为了偷懒,决定使用SDL2。优点如下
- 使用广泛, 资料众多
- 上手简单,跨平台
编辑器: lazarus 平台: ubuntu18.04
- 安装sdl2
sudo apt install libsdl2-dev libsdl2-gfx-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-net-dev libsdl2-ttf-dev
- 安装lazarus
sudo apt install lazarus make
- sdl2的pascal语言绑定
概览
开始之前需要了解游戏的大体的运行机制
部分平台的markdown不支持flowchart…
这就是游戏的基本框架,看起来很简单.
第一个示例
创建一个窗口,5秒后会自动关闭
Program test01;
{$mode objfpc}{$H+}Uses SysUtils,sdl2;Var pw : PSDL_Window;pr : PSDL_Renderer;
Begin// initSDL_Init(SDL_INIT_VIDEO);If SDL_WasInit(SDL_INIT_VIDEO)<>0 Then writeln('video init');pw := SDL_CreateWindow('Hello',SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,800,450,SDL_WINDOW_SHOWN);pr := SDL_CreateRenderer(pw,-1,0);// renderSDL_SetRenderDrawColor(pr,0,0,0,255);SDL_RenderClear(pr);SDL_RenderPresent(pr);SDL_Delay(5000);// cleanSDL_DestroyWindow(pw);SDL_DestroyRenderer(pr);SDL_Quit();
End.
接下来加入输入部分(暂时忽略物理计算)
isRunning := true;While isRunning DoBegin// handle inputIf SDL_PollEvent(@e)=1 ThenBeginCase e.Type_ Of SDL_QUITEV: isRunning := false;End;End;// TODO: do physics and then update// renderSDL_SetRenderDrawColor(pr,0,0,0,255);SDL_RenderClear(pr);SDL_RenderPresent(pr);End;
使用OOP来整理一下
Type TGame = ClassPrivate pw : PSDL_Window;pr : PSDL_Renderer;Public isRunning: boolean;Procedure Init(title : String;x,y,h,w,flags:integer );Procedure Render();Procedure Update();Procedure HandleEvents();Procedure Clean();
End;
Var g : TGame;
Beging := TGame.Create;g.Init('Hello',SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,600,400,SDL_WINDOW_SHOWN);While g.isRunning DoBeging.HandleEvents;g.Update;g.Render;End;g.Clean;g.Free;
End.
makefile
main:main.pasfpc -gh -Fusdl2 -Fl. main.pas
代码参考
.1
2. 渲染图片
渲染静态图片
新增一个Texture,然后Render出来
创建Texture,并获取尺寸
procedure TGame.Init(title: string; x, y, h, w, flags: integer);
begin.....pt := IMG_LoadTexture(pr, 'assets/run.png');SDL_QueryTexture(pt, nil, nil, @srcRect.w, @srcRect.h);destRect.x := srcRect.x;destRect.y := srcRect.y;destRect.w := srcRect.w;destRect.h := srcRect.h;......
end;
渲染出来
procedure TGame.Render();
beginSDL_SetRenderDrawColor(pr, 238, 238, 238, 255);SDL_RenderClear(pr);SDL_RenderCopy(pr, pt, @srcRect, @destRect);SDL_RenderPresent(pr);
end;
渲染动画
渲染动画就就快速交替渲染多张图片
procedure TGame.Update();
beginsrcRect.x := 96 * (round(SDL_GetTicks() / 100) mod 8);
end;
动画反转
本例中,如果人物需要朝相反方向行走,不用再搞一套素材
SDL_RenderCopyEx(pr, pt, @srcRect, @destRect,0, nil, SDL_FLIP_HORIZONTAL);
代码整理
代码味道
- Texture有多个,不能简单的使用变量。要有一个Texture容器
- 渲染时的Rect要和Texture的Render在一起防止错乱
新增一个TextureManager来统一的管理Texture,并解决以上两个问题
typeTTextureDict = specialize TFPGMap<string, PSDL_Texture>;TTextureManager = classprivatetextureMap: TTextureDict;publicdestructor Destroy();function Load(filename: string; id: string; pr: PSDL_Renderer): boolean;procedure Draw(id: string; x, y, w, h: integer; pr: PSDL_Renderer;flip: integer = 0);procedure DrawFrame(id: string; x, y, w, h, row, frame: integer;pr: PSDL_Renderer; flip: integer = 0);end;
由于多个TextureManager是不合适的 所以改为单例模式
private
constructor Init;
publicclass function Instance: TTextureManager;
完整代码见 .0/
3. 游戏对象管理
游戏中有很多类对象,例如:角色,敌人,NPC,陷阱,子弹,门等等.跟踪并处理它们之间的交互是一个有难度的事情。为了尽可能简化并使之容易维护,本节将尝试使用OOP(抽象继承多态等)来管理组织游戏对象
抽象与继承
首先定义一个公共的基类TGameObject
type TGameObject = class
protectedx, y: integer;w, h: integer;currentFrame, currentRow: integer;textureId: string;
publicprocedure Load(x,y,w,h:integer;textureId:string);procedure Draw(pr:PSDL_Renderer);procedure Update();procedure Clean;
end;
然后游戏中出现的对象都可以继承TGameObject
根据需要可以覆盖对应的方法
TPlayer = class(TGameObject)
end;TEnemy = class(TGameObject)end;
多态
接下来在Game中定义对象的列表,由于定义的是TGameObjectList,利用继承的特性就可以存储所有子类对象
type Game = class
private
objects:TGameObjectList;
end;
利用OOP的多态 修改Update,Render,这样就实现职责分离(每个对象负责自己的Draw,Update,Clean)
procedure TGame.Update();
vari: integer;
beginfor i := 0 to objects.Count - 1 dobeginobjects[i].Update();end;
end; procedure TGame.Render();
vari: integer;
beginSDL_RenderClear(pr);for i := 0 to objects.Count - 1 dobeginobjects[i].Draw(pr);end;SDL_RenderPresent(pr);
end;
对象的初始化也需要对应修改一下
procedure TGame.Init(title: string; x, y, h, w, flags: integer); b := (TTextureManager.Instance()).Load('assets/platform/arc2.png', 'animate', pr);go := TGameObject.Create;go.Load(0, 0, 40, 40, 'animate');objects.Add(go);player := TPlayer.Create;player.Load(50, 50, 40, 40, 'animate');objects.Add(player); end;
完整代码 .0/
4. 移动和用户输入
前言
目前已经可以在屏幕上渲染图片,并使用OOP的特性管理组织不同类的游戏对象
接下来需要添加用户控制功能.
常见的输入设备有: 鼠标 键盘 手柄
移动与向量
游戏的很多对象,即使没有用户输入,也要能移动。比如背景层,敌人等等。
为了处理移动的距离与方向,需要引入向量
使用向量修改TGameObject
TGameObject = class....publicposition: TVector2; // 位置velocity: TVector2; // 速度acceleration: TVector2; //加速度end;
然后修改Update函数
procedure TGameObject.Update();
beginvelocity := velocity + acceleration;position := position + velocity;
end;
用户输入捕获与管理
新增一个TInputHandle类专门用于管理用户的各种输入
TInputHandle = classpublicisQuit: boolean;destructor Destroy(); override;procedure Update;procedure Reset;function IsKeyDown(scancode: TSDL_Scancode): boolean;privatekeyState: PUInt8;procedure OnKeyEvent();end;
为了简单,目前只处理键盘事件
procedure TInputHandle.Update;
vare: TSDL_Event;
beginwhile SDL_PollEvent(@e) = 1 dobegincase e.Type_ ofSDL_QUITEV: isQuit := True; SDL_KEYUP, SDL_KEYDOWN: onKeyEvent;end;end;end;procedure TInputHandle.onKeyEvent();
beginkeyState := SDL_GetKeyboardState(nil);if isKeyDown(SDL_SCANCODE_ESCAPE) thenisQuit := True;
end;function TInputHandle.isKeyDown(scancode: TSDL_ScanCode): boolean;
beginif keyState <> nil thenResult := keyState[scancode] > 0elseResult := False;
end;
有了这些,我们就可以在每个TGameObject子类中使用不同的代码响应事件
procedure TPlayer.HandleInput;varv: TVector2;f: integer;
beginv.x := 0;v.y := 0;f := 0;if TInputHandle.Instance().IsKeyDown(SDL_SCANCODE_RIGHT) thenv.x := v.x + 1;if TInputHandle.Instance().IsKeyDown(SDL_SCANCODE_LEFT) thenv.x := v.x - 1;if v.x >= 0 thenf := SDL_FLIP_NONEelsef := SDL_FLIP_HORIZONTAL;if v.isZero thenangle := 0elseangle := round(RadToDeg(arccos(abs(v.x) / v.length)));writelnlog('angle:%f', [angle]);flip := f;if samevalue(v.x, 0) thenvelocity.x := lerp(velocity.x, 0, acceleration.x *2)elsevelocity.x := lerp(velocity.x, v.x * speed, acceleration.x);writelnlog('player v:%s,velocity:%s', [v.toString, velocity.toString]);
end;
完整代码
5. 游戏状态管理与有限状态机
前言
启动画面显示开发商和发行商,然后显示加载中,后台进行初始化。成功后显示菜单页面,其中可以选择开始游戏和游戏设置。点击开始则进入到各个游戏场景,游戏中可以暂停和恢复游戏, 角色死亡显示GameOver画面。
不同的状态下需要渲染的游戏对象是不一样的。都放到Game类中管理不太合适。一旦TGameObject对象变化,维护起来非常容易出错。
解决思路就是每个状态管理自己的TGameObject列表, 为了协调不同状态之间的切换和过度。新增一个状态管理类。
于是就有了本文的有限状态机
抽象状态基类
TGameState = classpublicstateId: string;procedure Update; virtual;procedure Render(); virtual;function OnExit: boolean; virtual;function OnEnter: boolean; virtual;protectedobjects: TGameObjectListend;
每个状态负责自己的TGameObject列表,并管理其渲染,更新
状态机
TGameStateMachine = classpublicconstructor Create;procedure PushState(state: TGameState);procedure ChangeState(state: TGameState);procedure PopState();procedure PopAndChangeState(state: TGameState);procedure Update;procedure Render();privatestates: TGameStateList;end;
状态机负责管理各种状态以及它们之间的过渡转换, Update和Render接口用于调用各个状态的Update和Render。并提供给TGame使用
实现状态
实现菜单状态前先要解决菜单项的绘制以及事件响应
TButtonClickEvent = procedure of object;TMenuButton = class(TGameObject)publicisReleased: boolean;OnClick: TButtonClickEvent;procedure Update; override;end;
在Update函数中判断鼠标的位置是否在矩形中以及左键是否按下来触发点击事件
procedure TMenuButton.Update;
varmp: TVector2;
begincurrentFrame := Ord(MOUSE_OUT);mp := (TInputHandle.Instance()).GetMousePosition();if (mp.x < position.x + w) and (mp.x > position.x) and(mp.y < position.y + h) and (mp.y > position.y) thenbeginif not (TInputHandle.Instance()).isMouseButtonDown(Ord(MOUSE_LEFT)) thenbeginisReleased := True;currentFrame := Ord(MOUSE_OVER);endelseif isReleased thenbeginwriteln('mouse click');currentFrame := Ord(MOUSE_CLICKED);if Assigned(OnClick) thenOnClick();isReleased := False;end;end
end;
接下来先实现刚进入时显示菜单的状态
TMenuState = class(TGameState)publicconstructor Create(r: PSDL_Renderer);function OnEnter: boolean; override;privateprocedure PlayClick;procedure ExitClick;procedure CreateBackground;end;
OnEnter 用于创建TGameObjectList并绑定点击事件
function TMenuState.OnEnter: boolean;
varbtn: TMenuButton;
begin(TTextureManager.Instance()).Load(pr, 'play_btn', 'assets/play_button.png');(TTextureManager.Instance()).Load(pr, 'exit_btn', 'assets/exit_button.png');CreateBackground();btn := TMenuButton.Create;btn.Load(100, 100, 400, 100, 'play_btn');btn.OnClick := @self.PlayClick;objects.Add(btn);btn := TMenuButton.Create;btn.Load(100, 300, 400, 100, 'exit_btn');btn.OnClick := @self.ExitClick;objects.Add(btn);Result := True;
end;
在每个点击事件代码中去创建目的状态
procedure TMenuState.PlayClick;
varstate: TGameState;
beginreadyState := TPlayState.Create(self.pr);
end;
并在基类的Update中处理状态的过渡
procedure TGameState.Update;
vari: integer;
beginwritelnlog(stateId);if Assigned(readyState) thenbeginwritelnlog(readyState.stateId);(TGameStateMachine.Instance()).changeState(readyState);exit;end;for i := 0 to objects.Count - 1 doobjects[i].Update;
end;
最后就是修改TGame类,去除TGameObjectList的管理
procedure TGame.Update();
beginif not isRunning thenexit; (TGameStateMachine.Instance()).Update;
end;
procedure TGame.Render();
beginif not isRunning thenexit;SDL_RenderClear(pr);(TGameStateMachine.Instance()).Render();SDL_RenderPresent(pr);
end;
procedure TGame.Init(title: string; x, y, h, w, flags: integer);
var state:TGameState;
begin.....state := TMenuState.Create(pr);(TGameStateMachine.Instance()).pushState(state);....
end;
至于PlayState,PauseState依葫芦画瓢实现就可以了
完整代码
更多推荐
Pascal游戏开发入门
发布评论