Android 基本游戏循环






4.80/5 (18投票s)
本文将解释如何在 Android 平台上实现基本的游戏原理。
来源
GitHub: https://github.com/Pavel-Durov/CodeProject-Android-Basic-Game-Loop
直接
目录
引言
本文将解释如何仅使用受管理的 Java 代码和 SurfaceView
类在 Android 平台上创建一款简单游戏。
我们将研究基本概念,并通过触摸事件创建显示交互。
我们将实现一个 2D 泡泡游戏的基本阶段,其中包括一个向我们的触摸方向射击泡泡的加农炮。
您的游戏将看起来像这样
基本游戏概念
我们需要在程序中建立几个模块
1. 从用户获取输入数据。
2. 将该数据存储在程序的某个位置。
3. 在屏幕上显示结果。
程序模块通信图
DisplayThread 循环图
显示对象
显示在我们 Android 设备屏幕上的每个图形都是一个具有自己逻辑的 Java 对象。在我们的游戏中,我们有 3 种这种类型的对象:加农炮、泡泡和瞄准器。
每个对象都有其 x 和 y 坐标,用于标识其在屏幕上的位置。不断移动的对象(如泡泡)会保留其 deltas,用于标识其方向。
游戏活动
我们将创建一个活动来监听触摸事件(用户输入),并将其内容视图设置为我们稍后将实现的自定义视图。
让我们检查一下我们的 GameActivity
onCreate() 方法
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
//sets the activity view as GameView class
SurfaceView view = new GameView(this, AppConstants.GetEngine());
setContentView(view);
getActionBar().hide();
}
通常我们会在 onCreate
方法中看到类似这样的内容
//setting content view from resource xml file
setContentView(R.layout.main_activity);
此方法将 xml 布局设置为活动的内容视图,该视图将自动生成为 View
。
在我们的例子中,我们将内容视图设置为我们的自定义 View 类,因为我们需要在 View
上实现我们自己的方法和修改,这些是常规约定无法执行的。
接下来,我们将通过重写 onTouchEvent(MotionEvent event)
方法来监听触摸事件,并根据 MotionEvent
值调用我们的方法。
OnTouch 方法
/*activates on touch move event*/
private void OnActionMove(MotionEvent event)
{
int x = (int)event.getX();
int y = (int)event.getY();
if(GetIfTouchInTheZone(x, y))
{
AppConstants.GetEngine().SetCannonRotaion(x, y);
}
AppConstants.GetEngine().SetLastTouch(event.getX(), event.getY());
}
/*activates on touch up event*/
private void OnActionUp(MotionEvent event)
{
int x = (int)event.getX();
int y = (int)event.getY();
if(GetIfTouchInTheZone(x, y))
{
AppConstants.GetEngine().SetCannonRotaion(x, y);
AppConstants.GetEngine().SetLastTouch(FingerAim.DO_NOT_DRAW_X
,FingerAim.DO_NOT_DRAW_Y);
AppConstants.GetEngine().CreateNewBubble(x,y);
}
}
/*activates on touch down event*/
private void OnActionDown(MotionEvent event)
{
AppConstants.GetEngine()
.SetLastTouch(event.getX(), event.getY());
}
当用户触摸我们的活动屏幕时,会调用这些方法。
它们调用我们 GameEngine
对象中相应的方法。这将改变我们的业务逻辑,并在用户和我们的应用程序之间创建交互。
GameView 类
GameView
类继承自 SurfaceView
,而 SurfaceView
又继承自 View
类,因此我们可以将其设置为我们的活动内容视图。
此类实现了我们的显示逻辑。
创建时(当在我们的 GameActivity
中调用 new 时),GameView
初始化并启动 DisplayThread
对象。
有关 SurfaceView
类的更多信息
https://developer.android.com.cn/reference/android/view/SurfaceView.html
我们的 GameView
实现了 SurfaceHolder.Callback
接口
@Override
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3)
{
/*DO NOTHING*/
}
在此方法中,我们不执行任何操作,因为它不符合我们的目的。但是,我们必须在我们的类中实现它,因为它是接口的一部分。
GameView 中所有被重写的方法都是 SurfaceHolder.Callback 接口
的一部分。
@Override
public void surfaceCreated(SurfaceHolder arg0)
{
//Starts the display thread
if(!_displayThread.IsRunning())
{
_displayThread = new DisplayThread(getHolder(), _context);
_displayThread.start();
}
else
{
_displayThread.start();
}
}
当 GameActivity
内容视图初始化时,即调用 OnCreate()
、onStart()
或 onResume()
方法时,会调用 surfaceCreated()
,它只是简单地启动 DisplayThread
。
@Override
public void surfaceDestroyed(SurfaceHolder arg0)
{
//Stop the display thread
_displayThread.SetIsRunning(false);
AppConstants.StopThread(_displayThread);
}
当调用 GameActivity
的 OnPause()、OnStop() 或 OnDestroy() 生命周期方法时,会调用 SurfaceDestroyed
,此方法停止 DisplayThread
。
DisplayThread 类
DisplayThread
是刷新我们屏幕的线程。这个神奇的过程发生在它的 run()
方法中。
DisplayThread 屏幕渲染
@Override
public void run()
{
//Looping until the boolean is false
while (_isOnRun)
{
//Updates the game objects buisiness logic
AppConstants.GetEngine().Update();
//locking the canvas
Canvas canvas = _surfaceHolder.lockCanvas(null);
if (canvas != null)
{
//Clears the screen with black paint and draws
//object on the canvas
synchronized (_surfaceHolder)
{
canvas.drawRect(0, 0,
canvas.getWidth(),canvas.getHeight(), _backgroundPaint);
AppConstants.GetEngine().Draw(canvas);
}
//unlocking the Canvas
_surfaceHolder.unlockCanvasAndPost(canvas);
}
//delay time
try
{
Thread.sleep(DELAY);
}
catch (InterruptedException ex)
{
//TODO: Log
}
}
}
只要布尔变量 _isOnRun
没有设置为 false,run()
方法就会在其 while
循环中循环,这只有通过调用 SetIsRunning(boolean state)
方法才能实现。
SetIsRunning(boolean state)
仅从我们 GameView
类中实现的 SurfaceHolder.Callback
接口方法中调用。
run()
方法进入 while
循环后做的第一件事是调用 GameEngine
对象中的 Update()
方法。这将更新我们的业务逻辑(在我们的例子中是推进气球)。
接下来它锁定画布(来自我们的 SurfaceView
类),用黑色背景绘制整个视图,并将画布传递给 GameEngine Draw()
方法。这将以更新的参数在我们的显示器上显示我们的对象。
最后,它解锁画布,并休眠指定的时间,这被称为 fps(每秒帧数)。
我们的大脑每秒处理大约 20 帧,我们的代码每 45 毫秒刷新一次屏幕,这使得它达到 22 fps。
在我们的实现中,我们使用硬编码值调用 Thread.sleep()
,期望它暂停一段时间然后继续执行逻辑。
我们需要意识到渲染和更新游戏所需的时间是不一致的。
例如,如果您需要前进一个泡泡,它不会像在屏幕上前进 550 个泡泡那样花费相同的时间。这使得我们的游戏渲染不流畅,因为循环执行时间彼此不同。
这个问题可以通过在循环开始时获取时间戳并从睡眠时间中减去它来解决,这将使我们的每个循环执行时间相同。
*我没有在我们的 DisplayThread
实现中包含该解决方案,因为我想保持其简单。
GameEngine 类
这个对象负责所有游戏业务逻辑,它保存了所有显示对象的实例(在我们的例子中:泡泡、瞄准器和加农炮)。
Update() 和 Draw(Canvas canvas) 方法
public void Update()
{
AdvanceBubbles();
}
Update()
方法更新我们游戏的所有对象,它推进泡泡。然而,它不更新 Cannon
对象,因为它的旋转值仅在用户触摸屏幕时才会改变。
public void Draw(Canvas canvas)
{
DrawCanon(canvas);
DrawBubles(canvas);
DrawAim(canvas);
}
Draw(Canvas canvas)
方法是在 DisplayThread
类调用 Update()
之后调用的。Draw(Canvas canvas)
方法将所有与游戏显示相关的对象绘制到给定的画布上。
其他 GameEngine 方法
public void SetCannonRotaion(int touch_x, int touch_y) { float cannonRotation = RotationHandler .CannonRotationByTouch(touch_x, touch_y, _cannon); _cannon.SetRotation(cannonRotation); }
SetCannonRotaion()
方法在触摸事件发生时从 GameActivity 调用。
我们根据触摸事件坐标直接旋转位图。
第一阶段是用户输入,它会调用 GameEngine
中的 SetCannonRotation()
,后者再调用 RotationHandler.CannonRotationByTouch()
和 _cannon.SetRotation()
并传入结果。
public static float CannonRotationByTouch(int touch_x, int touch_y, Cannon cannon)
{
float result = cannon.GetRotation();
if(CheckIfTouchIsInTheZone(touch_x, touch_y, cannon))
{
if(CheckIsOnLeftSideScreen(touch_x))
{
int Opposite = touch_x - cannon.GetX();
int Adjacent = cannon.GetY() - touch_y;
double angle = Math.atan2(Opposite, Adjacent);
result = (float)Math.toDegrees(angle);
}
else
{
int Opposite = cannon.GetX() - touch_x;
int Adjacent = cannon.GetY() - touch_y;
double angle = Math.atan2(Opposite, Adjacent);
result = ANGLE_360 - (float)Math.toDegrees(angle) ;
}
}
return result;
}
CannonRotationByTouch()
方法使用了一点几何学,它识别触摸发生在加农炮的右侧还是左侧,并根据其位置计算角度。
我们使用正切来计算角度,因为我们可以计算加农炮位置和触摸坐标之间创建的直角三角形的邻边和对边长度。
请注意,当我们使用 Matrix 对象旋转加农炮位图时
public static Bitmap RotateBitmap(Bitmap source, float angle)
{
Matrix matrix = new Matrix();
matrix.postRotate(angle);
return Bitmap.createBitmap
(
source,
0, 0,
source.getWidth(),
source.getHeight(),
matrix,
true
);
}
我们将旋转后的加农炮调整为我们原来的宽度和高度。这就是为什么我们的加农炮(Android 图标)在位图旋转时会变小。
这样做是不对的;我们应该适当调整旋转图像的新尺寸。我们在此处没有实现它,只是为了简化我们的代码。
public void CreateNewBubble(int touchX, int touchY)
{
synchronized (_sync)
{
_bubles.add
(
new Bubble
(
_cannon.GetX(),
_cannon.GetY(),
_cannon.GetRotation(),
touchX,
touchY
)
);
}
}
CreateNewBubble()
也在触摸事件中被调用,它创建新的 Bubble 对象并添加到泡泡列表中,Update()
和 Draw()
方法将遍历该列表。
摘要
我们创建了一个非常基础的游戏,当然还有很多我们可以实现的地方,目前它对用户来说并不是一个非常有吸引力的游戏。但是,如果您理解了游戏循环的基本思想,您可以根据自己的意愿继续在该平台上开发。
通过仅更改 GameEngine
中 Update
和 Draw
方法的实现以及涉及的对象,它可以很容易地更改为任何其他 2D 游戏概念,同时保持游戏循环的基础不变。