游戏循环是游戏的操作体,帧率是结果。没有定义的游戏循环就无法制作游戏,不测量帧率就无法判断性能。
游戏开发的这两个方面在任何游戏开发项目中都很常见。然而,游戏循环的可扩展性和性质在不同的设备上有所不同,并且可能有不同的尺度来测量不同平台上的帧速率。
对于本机开发,游戏循环仅由开发人员创建和维护。然而,在大多数游戏引擎中,循环已经定义了所有必要的控制和范围。
我们将通过以下主题详细了解游戏开发的这两个最重要的部分:
- 游戏循环介绍
- 使用安卓软件开发工具包创建一个示例游戏循环
- 游戏生命周期
- 游戏更新和用户界面
- 中断处理
- 游戏状态机的一般概念
- FPS 系统
- 硬件依赖性
- 性能和内存之间的平衡
- 控制 FPS
游戏循环是依次执行用户输入、游戏更新和渲染的核心循环。这个循环理想情况下每帧运行一次。所以,游戏循环是运行一个有帧率控制的游戏最重要的部分。
典型的游戏循环有三个步骤:
该部分检查游戏的 UI 系统是否有任何外部输入已经被给予游戏。它为下一次更新设置了需要在游戏中进行的更改。在不同的硬件平台上,游戏循环的这一部分变化最大。为不同的输入类型创建通用功能以形成标准始终是最佳实践。
输入系统不被视为游戏循环的一部分;然而,用户给定的输入检测是游戏循环的一部分。该系统持续监控输入系统,无论事件是否发生。
当活动的游戏循环正在运行时,用户可以在游戏过程中的任何时间点触发任何事件。通常,有由输入系统维护的队列。每个队列代表不同类型的可能输入事件,如触摸、按键、传感器读数等。
用户输入监视器在循环序列之后的特定间隔检查那些队列。如果它在队列中找到任何事件,它会进行所需的更改,这些更改将对游戏循环中的下一个更新调用产生影响:
用户输入工作原理
完整的游戏状态由游戏循环的游戏更新部分管理和维护。该部分还负责运行游戏逻辑、游戏状态的更改、加载/卸载素材以及设置渲染管道。
游戏控制通常由游戏更新部分管理。通常,主游戏管理器在这个游戏更新部分的顶层工作。我们在前一节讨论了游戏程序结构。
任何游戏一次运行一个特定的状态。状态可以通过用户输入或任何自动人工智能算法来更新。所有的人工智能算法都在一帧一帧的游戏更新周期中工作。
如前所述,状态可以从游戏更新中更新。状态也是由游戏更新发起和破坏的。初始化和销毁每个状态发生一次,每个游戏周期可以调用一次状态更新。
状态更新调用流
游戏循环内的渲染部分负责设置渲染管道。游戏循环的这一部分没有运行任何更新或 AI 算法。
曾经有一段时间,开发人员可以完全控制渲染管道。开发者可以操纵和设置每个顶点。现代游戏开发系统与这个渲染系统关系不大。图形库负责渲染系统的所有控制。然而,在很高的层次上,开发人员只能设置渲染顶点的顺序和数量。
渲染是帧率控制中最重要的角色之一,保持其他连续过程不变。从处理的角度来看,显示和内存操作的执行时间最长。
典型的安卓图形渲染遵循 OpenGL 管道:
安卓 SDK 开发从一个活动开始,游戏在单个或多个视图上运行。大多数时候,它被认为只有一个视图来运行游戏。
不幸的是,安卓软件开发工具包没有提供预定义的游戏循环。然而,循环可以用许多方式创建,但基本机制保持不变。
在安卓软件开发工具包库中,View
类包含一个抽象方法OnDraw()
,每个可能的渲染调用都在其中排队。图形中的任何更改都会调用此方法,这会使以前的渲染管道失效。
逻辑如下:
我们来看看用安卓View
创建的一个基本游戏循环。这里,从安卓View
扩展了一个自定义视图:
/*Sample Loop created within OnDraw()on Canvas
* This loop works with 2D android game development
*/
@Override
public void onDraw(Canvas canvas)
{
//If the game loop is active then only update and render
if(gameRunning)
{
//update game state
MainGameUpdate();
//set rendering pipeline for updated game state
RenderFrame(canvas);
//Invalidate previous frame, so that updated pipeline can be
// rendered
//Calling invalidate() causes recall of onDraw()
invalidate();
}
else
{
//If there is no active game loop
//Exit the game
System.exit(0);
}
}
在目前安卓游戏开发的时代,开发者用SurfaceView
代替View
。SurfaceView
是从View
继承而来的,更适合使用 Canvas 制作的游戏。在这种情况下,定制视图从SurfaceView
扩展并实现SurfaceHolder.Callback
界面。在这种情况下,有三种方法被覆盖:
/* Called When a surface is changed */
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
}
/* Called on create of a SurfaceView */
@Override
public void surfaceCreated(SurfaceHolder holder)
{
}
/* Called on destroy of a SurfaceView is destroyed */
@Override
public void surfaceDestroyed(SurfaceHolder holder)
{
}
开发游戏时,开发者不需要每次都改变表面。这就是为什么surfaceChanged
方法应该有一个空的身体来作为一个基本的游戏循环。
我们需要创建一个定制的游戏线程并覆盖run()
方法:
public class BaseGameThread extends Thread
{
private boolean isGameRunning;
private SurfaceHolder currentHolder;
private MyGameState currentState;
public void activateGameThread(SurfaceHolder holder, MyGameState state)
{
currentState = state;
isGameRunning = true;
currentHolder = holder;
this.start();
}
@Override
public void run()
{
Canvas gameCanvas = null;
while(isGameRunning)
{
//clear canvas
gameCanvas = null;
try
{
//locking the canvas for screen pixel editing
gameCanvas = currentHolder.lockCanvas();
//Update game state
currentState.update();
//render game state
currentState.render(gameCanvas);
}
catch(Exception e)
{
//Update game state without rendering (Optional)
currentState.update();
}
}
}
}
现在,我们从定制的SurfaceView
类开始新创建的游戏循环:
public myGameCanvas extends SurfaceView implements SurfaceHolder
{
//Declare thread
private BaseGameThread gameThread;
private MyGameState gameState;
@Override
public void surfaceCreated(SurfaceHolder holder)
{
//Initialize game state
gameState = new MyGameState();
//Instantiate game thread
gameThread = new BaseGameThread();
//Start game thread
gameThread. activateGameThread(this.getHolder(),gameState);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
}
@Override
public void surfaceDestroyed(SurfaceHolder holder)
{
}
}
可以有许多方法来实现一个游戏循环。但是,基本方法遵循这里提到的两种方式中的任何一种。一些开发者更喜欢在游戏视图中实现游戏线程。处理输入是游戏循环的另一个重要部分。我们将在本章后面讨论这个主题。
这个游戏循环的另一部分是每秒帧数 ( FPS ) 管理。最常见的机制之一是使用Thread.sleep()
计算循环以固定速率执行的时间。一些开发人员创建了两种类型的更新机制:一种基于 FPS,另一种基于每帧无延迟。
大多数情况下,基于物理的游戏需要一个遵循实时间隔的更新机制,以便在所有设备上统一运行。
对于小规模开发,行业中很少有开发人员遵循第一种方法,但不遵循典型的循环。该系统根据所需操作使当前抽取无效。在这种情况下,游戏循环不依赖于固定的 FPS。
安卓游戏生命周期除了游戏循环机制外,几乎和其他任何应用的生命周期都差不多。大多数情况下,应用状态会随着外部干扰而改变。状态可以以其他方式操纵,游戏有算法或人工智能,能够干扰主游戏周期。
一个安卓游戏用一个活动初始化。onCreate()
方法用于初始化。然后,游戏线程开始并进入游戏循环。然后,游戏循环可以被外部中断中断。
在游戏开发的情况下,保存当前游戏状态,适当暂停循环和线程,总是一个不错的做法。恢复游戏后,应该很容易回到上一个状态。
我们之前已经介绍了一些更新和接口机制。运行中的游戏状态可以通过用户输入或内部人工智能算法来改变:
大多数情况下,游戏更新每帧调用一次,或者在固定的时间间隔后调用一次。无论哪种方式,算法都会改变游戏状态。您已经了解了用户输入队列。在每个游戏循环周期中,都会检查输入队列。
例如,带有触摸界面的移动游戏循环的工作原理如下:
/* import proper view and implement touch listener */
public class MyGameView extends View implements View.OnTouchListener
/* declare game state */
private MyGameState gameState;
/* set listener */
public MyGameView (Context context)
{
super(context);
setOnTouchListener(this);
setFocusableInTouchMode(true);
gameState = new MyGameState();
}
/* override onTouch() and call state update on individual touch events */
@Override
public boolean onTouch(View v, MotionEvent event)
{
if(event.getAction() == MotionEvent.ACTION_UP)
{
//call changes in current state on touch release
gameState.handleInputTouchRelease((int)event.getX(), (int)event.getY());
return false;
}
else if(event.getAction() == MotionEvent.ACTION_DOWN)
{
//call changes in current state on touch begin
gameState.handleInputTouchEngage((int)event.getX(), (int)event.getY());
}
else if(event.getAction() == MotionEvent.ACTION_MOVE)
{
//call changes in current state on touch drag
gameState.handleInputTouchDrag((int)event.getX(), (int)event.getY());
}
return true;
}
现在,让我们看看具有相同方法的输入队列系统:
Point touchBegin = null;
Point touchDragged = null;
Point touchEnd = null;
@Override
public boolean onTouch(View v, MotionEvent event)
{
if(event.getAction() == MotionEvent.ACTION_UP)
{
touchEnd = new Point(int)event.getX(), (int)event.getY());
return false;
}
else if(event.getAction() == MotionEvent.ACTION_DOWN)
{
touchBegin = new Point(int)event.getX(), (int)event.getY());
}
else if(event.getAction() == MotionEvent.ACTION_MOVE)
{
touchDragged = new Point(int)event.getX(), (int)event.getY());
}
return true;
}
/* declare checking input mechanism */
private void checkUserInput()
{
if(touchBegin != null)
{
//call changes in current state on touch begin
gameState. handleInputTouchEngage (touchBegin);
touchBegin = null;
}
if(touchDragged != null)
{
//call changes in current state on touch drag
gameState. handleInputTouchDrag (touchDragged);
touchDragged = null;
}
if(touchEnd != null)
{
//call changes in current state on touch release
gameState.handleInputTouchRelease (touchEnd);
touchEnd = null;
}
}
/* finally we need to invoke checking inside game loop */
@Override
public void onDraw(Canvas canvas)
{
//If the game loop is active then only update and render
if(gameRunning)
{
//check user input
checkUserInput();
//update game state
MainGameUpdate();
//set rendering pipeline for updated game state
RenderFrame(canvas);
//Invalidate previous frame, so that updated pipeline can be
// rendered
//Calling invalidate() causes recall of onDraw()
invalidate();
}
else
{
//If there is no active game loop
//Exit the game
System.exit(0);
}
}
对于SurfaceView
游戏循环方法也可以重复相同的过程。
游戏循环是一个连续的过程。每当中断发生时,都需要暂停每个正在运行的线程,并保存游戏的当前状态,以确保它正常恢复。
在安卓系统中,任何中断都是从onPause()
开始触发的:
@Override
protected void onPause()
{
super.onPause();
// pause and save game loop here
}
// When control is given back to application, then onResume() is // called.
@Override
protected void onResume()
{
super.onResume();
//resume the game loop here
}
现在,我们需要改变实际游戏循环运行的类。
首先,声明一个布尔值来指示游戏是否暂停。然后,在游戏循环中放一张支票。之后,创建一个静态方法来处理这个变量:
private static boolean gamePaused = false;
@Override
public void onDraw(Canvas canvas)
{
if(gameRunning && ! gamePaused)
{
MainGameUpdate();
RenderFrame(canvas);
invalidate();
}
else if(! gamePaused)
{
//If there is no active game loop
//Exit the game
System.exit(0);
}
}
public static void enableGameLoop(boolean enable)
{
gamePaused = enable;
if(!gamePaused)
{
//invalidation of previous draw has to be called from static
// instance of current View class
this.invalidate();
}
else
{
//save state
}
}
游戏状态机在游戏循环的更新周期内运行。游戏状态机是将所有游戏状态绑定在一起的机制。在旧技术中,这是典型的线性控制流程。然而,在现代开发过程中,它可以是在多个线程中运行的并行控制。在游戏开发的旧架构中,鼓励只有一个游戏线程。开发人员过去避免并行处理,因为它容易受到游戏循环和计时器管理的影响。然而,即使在现代开发中,许多开发人员仍然倾向于尽可能使用单线程进行游戏开发。在各种工具和高级脚本语言的帮助下,大多数游戏开发人员现在使用虚拟并行处理系统。
一个简单的游戏状态机的过程之一是创建一个公共的状态接口,并为每个游戏状态覆盖它。这样,管理游戏循环内部的状态就变得容易了。
让我们看看一个简单的游戏状态机管理器的循环。该经理应执行四项主要功能:
- 创建状态
- 更新状态
- 呈现状态
- 改变状态
一个示例实现可能如下所示:
public class MainStateManager
{
private int currentStateId;
//setting up state IDs
public Interface GameStates
{
public static final int STATE_1 = 0;
public static final int STATE_2 = 1;
public static final int STATE_3 = 2;
public static final int STATE_4 = 3;
}
private void initializeState(int stateId)
{
currentStateId = stateId;
switch(currentStateId)
{
case STATE_1:
// initialize/load state 1
break;
case STATE_2:
// initialize/load state 2
break;
case STATE_3:
// initialize/load state 3
break;
case STATE_4:
// initialize/load state 4
break;
}
}
}
/*
* update is called in every cycle of game loop.
* make sure that the state is already initialized before updating the state
*/
private void updateState()
{
switch(currentStateId)
{
case STATE_1:
// Update state 1
break;
case STATE_2:
// Update state 2
break;
case STATE_3:
// Update state 3
break;
case STATE_4:
// Update state 4
break;
}
}
/*
* render is called in every cycle of game loop.
* make sure that the state is already initialized before updating the state
*/
private void renderState()
{
switch(currentStateId)
{
case STATE_1:
// Render state 1
break;
case STATE_2:
// Render state 2
break;
case STATE_3:
// Render state 3
break;
case STATE_4:
// Render state 4
break;
}
}
/*
* Change state can be triggered from outside of manager or from any other state
* This should be responsible for destroying previous state and free memory and initialize new state
*/
public void changeState(int nextState)
{
switch(currentStateId)
{
case STATE_1:
// Destroy state 1
break;
case STATE_2:
// Destroy state 2
break;
case STATE_3:
// Destroy state 3
break;
case STATE_4:
// Destroy state 4
break;
}
initializeState(nextState);
}
}
在某些情况下,开发人员也通过状态管理器将输入信号传递给特定的状态。
在游戏开发和游戏行业的案例中,FPS 非常重要。游戏质量的衡量很大程度上取决于 FPS 计数。简单来说,游戏的 FPS 越高越好。游戏的 FPS 取决于指令和渲染的处理时间。
执行一次游戏循环需要一些时间。让我们看一下游戏循环中 FPS 管理的示例实现:
long startTime;
long endTime;
public final int TARGET_FPS = 60;
@Override
public void onDraw(Canvas canvas)
{
if(isRunning)
{
startTime = System.currentTimeMillis();
//update and paint in game cycle
MainGameUpdate();
//set rendering pipeline for updated game state
RenderFrame(canvas);
endTime = System.currentTimeMillis();
long delta = endTime - startTime;
long interval = (1000 - delta)/TARGET_FPS;
try
{
Thread.sleep(interval);
}
catch(Exception ex)
{}
invalidate();
}
}
在前面的例子中,我们首先记录了循环执行前的时间(startTime
),然后记录了执行后的时间(endTime
)。然后我们计算了执行死刑的时间(delta
)。我们已经知道保持最大帧速率所需的时间量(interval
)。所以,在剩下的时间里,我们让游戏线程进入睡眠状态,然后再执行。这也可以应用于不同的游戏循环系统。
在使用SurfaceView
的同时,我们可以在run()
方法中声明游戏循环内部的 FPS 系统:
long startTime;
long endTime;
public final int TARGET_FPS = 60;
@Override
public void run()
{
Canvas gameCanvas = null;
while(isGameRunning)
{
startTime = System.currentTimeMillis();
//clear canvas
gameCanvas = null;
try
{
//locking the canvas for screen pixel editing
gameCanvas = currentHolder.lockCanvas();
//Update game state
currentState.update();
//render game state
currentState.render(gameCanvas);
endTime = System.currentTimeMillis();
long delta = endTime - startTime;
long interval = (1000 - delta)/TARGET_FPS;
try
{
Thread.sleep(interval);
}
catch(Exception ex)
{}
}
Catch(Exception e)
{
//Update game state without rendering (Optional)
currentState.update();
}
}
}
在这个过程中,我们限制了 FPS 计数,并尝试在预定义的 FPS 上执行游戏循环。该系统的一个主要缺点是该机制严重依赖硬件配置。对于不能在预定义的 FPS 上运行循环的慢速硬件系统,该系统没有任何影响。这是因为间隔时间大多为零或小于零,所以没有每帧周期。
我们之前已经讨论过硬件配置在 FPS 系统中起主要作用。如果硬件不能以一定的频率运行某组指令,那么任何开发人员都不可能在目标 FPS 上运行游戏。
让我们列出占用游戏大部分处理时间的任务:
- 显示或渲染
- 内存加载/卸载操作
- 逻辑运算
显示处理大部分取决于图形处理器和什么都需要显示。当涉及到与硬件的交互时,这个过程会变得很慢。使用着色器操作和映射渲染每个像素需要时间。
有时候运行一个帧率为 12 的游戏很困难。然而,在现代世界中,一款卓越的显示质量游戏需要以 60 的帧率运行。这只是硬件质量的问题。
大显示器需要大量的高速缓存。因此,例如,具有大而密集的显示器和低高速缓冲存储器的硬件不能保持良好的显示质量。
内存是系统的硬件组件。同样,与内存组件交互需要更多时间。从开发人员的角度来看,当我们分配内存、释放内存以及读取或写入操作时,需要花费时间。
从游戏的发展来看,四种记忆类型最为重要:
- 堆内存
- 栈存储器
- 寄存器存储器
- 只读存储器
堆内存是用户自定义手动管理的内存。必须手动分配和释放这些内存。在安卓的情况下,垃圾收集器负责释放内存,内存被标记为不被引用。该内存位置是随机存取内存类别中最慢的。
内存的这个段用于方法内部声明的元素。这个内存段的分配和解除分配由程序解释器自动完成。该内存段仅适用于本地成员。
寄存器内存是所有里面最快的。寄存器内存用于存储当前进程的数据和常用数据。在寄存器内存更好更快的设备情况下,游戏开发者可以实现更高的帧率。
只读存储器 ( ROM ) 为永久存储器。尤其是在游戏开发中,大量的素材存储在只读存储器中。在装载/卸载这些素材的过程中,需要花费最长的时间。一个程序需要将必要的数据从只读存储器加载到随机存取存储器中。因此,更快的只读存储器有助于在加载/卸载操作期间实现更好的 FPS。
开发人员应该定义指令,使他们能够以最有效的方式使用硬件。用专业术语来说,每一条指令都以二进制指令的形式进入堆栈。处理器在一个时钟周期内执行一条指令。
例如,让我们来看看一个构造不良的逻辑指令:
char[] name = "my name is android";
for(int i = 0; i < name.length; i ++)
{
//some operation
}
每次调用length
并使用后递增运算符会增加对处理器的指令,最终会增加执行时间。现在,看看这段代码:
char[] name = "my name is android";
int length = name.length;
for(int i = 0; i < length; ++ i)
{
//some operation
}
这段代码执行了相同的任务;然而,这种方法大大减少了处理开销。这段代码做出的唯一妥协是阻塞一个整数变量的内存,并保存大量与length
相关的嵌套任务。
时钟速度更好的处理器可以更快地执行任务,这直接意味着更好的 FPS。但是,管理任务量取决于开发人员,如前面的示例所示。
每个处理器都有一个数学处理单元。处理器的功率因处理器而异。所以,开发人员总是需要检查数学表达式,以了解它是否可以简化。
正如你前面所学的,记忆操作需要很多时间。但是,开发人员的内存总是有限的。所以,在性能和内存之间取得平衡是极其必要的。
将任何素材从只读存储器加载或卸载到随机存取存储器需要时间,因此建议您不要对依赖 FPS 的游戏进行此类操作。该操作对 FPS 影响很大。
假设一个游戏在运行一个游戏状态时需要大量素材,而目标设备的可用堆有限。在这种情况下,开发人员应该对素材进行分组。只有在需要的情况下,才能在运行状态的游戏中加载小素材。
有时,许多开发人员预加载所有素材并从缓存中使用它。这种方法使游戏更加流畅和快速。然而,如果发生中断,在该特定游戏状态不需要的缓存中加载素材可能会使游戏崩溃。安卓操作系统完全有权清除非活动或最小化应用占用的内存。当中断发生时,游戏进入最小化状态。如果一个新的应用需要内存,并且没有可用的空闲内存,那么安卓操作系统会杀死不活动的应用,并为新的应用释放内存。
因此,根据游戏状态将一组素材分成几部分总是一个好的做法。
我们已经看到了一些定义 FPS 系统的方法。我们也已经讨论了这个系统的主要缺点。因此,我们可以根据当前游戏循环周期中生成的实时 FPS 来操纵游戏循环:
long startTime;
long endTime;
public static in ACTUAL_FPS = 0;
@Override
public void onDraw(Canvas canvas)
{
if(isRunning)
{
startTime = System.currentTimeMillis();
//update and paint in game cycle
MainGameUpdate();
//set rendering pipeline for updated game state
RenderFrame(canvas);
endTime = System.currentTimeMillis();
long delta = endTime - startTime;
ACTUAL_FPS = 1000 / delta;
invalidate();
}
}
现在,让我们看看混合 FPS 系统,其中我们将最大 FPS 限制为 60。否则,可以通过实际的 FPS 操纵游戏:
long startTime;
long endTime;
public final int TARGET_FPS = 60;
public static int ACTUAL_FPS = 0;
@Override
public void onDraw(Canvas canvas)
{
if(isRunning)
{
startTime = System.currentTimeMillis();
//update and paint in game cycle
MainGameUpdate();
//set rendering pipeline for updated game state
RenderFrame(canvas);
endTime = System.currentTimeMillis();
long delta = endTime - startTime;
//hybrid system begins
if(delta < 1000)
{
long interval = (1000 - delta)/TARGET_FPS;
ACTUAL_FPS = TARGET_FPS;
try
{
Thread.sleep(interval);
}
catch(Exception ex)
{}
}
else
{
ACTUAL_FPS = 1000 / delta;
}
invalidate();
}
}
游戏循环主要是游戏开发的一种逻辑方法。在许多情况下,开发人员不会选择这样的机制。有些游戏可能是典型的交互式游戏,没有连续运行的算法。在这种情况下,可能不需要游戏循环。游戏状态可以根据给游戏系统的输入来更新。
然而,例外不能成为例子。这就是为什么不管游戏设计如何,遵循游戏循环来保持开发标准是一种工业方法。
你在这里学习了游戏循环和游戏状态管理。开发人员可以自由地以不同的方式发明和执行游戏循环。有许多游戏引擎有不同的方法来控制游戏循环和管理游戏状态。游戏循环和状态管理的思想和概念可能会根据游戏需求而改变。
但是,开发人员应该始终记住,他们使用的技术不应该影响游戏性能和 FPS。除此之外,开发人员需要保持代码的可读性和灵活性。有些方法可能会消耗更多内存,运行速度更快,反之亦然。安卓有各种硬件配置,所以可能不是所有硬件都支持相同的处理和内存。最后,平衡内存和性能是创造更好游戏的关键。
我们将在后面的章节中深入研究性能和内存管理。我们将尝试从不同的角度来看待游戏开发的这些细分领域,例如 2D/3D 游戏、VR 游戏、优化技术等等。