我刚刚开始在Android中进行游戏开发,而我正在开发一款超级简单的游戏。
游戏基本上就像飞扬的小鸟。
我设法让一切工作,但我得到了很多口吃和滞后。
我用于测试的手机是LG G2,所以它应该并且确实运行的游戏比这更重,更复杂。
基本上有4个“障碍物”是相互分开的全屏宽度 当游戏开始时,障碍物开始以恒定速度移动(朝向角色)。玩家角色的x值在整个游戏中是一致的,而其y值会发生变化。
滞后主要发生在角色穿过障碍物时(有时也会在障碍物之后)。发生的事情是,每个游戏状态图中都存在不均匀的延迟,导致运动中出现断断续续的情况。
我的想法是,有一行或多行代码会导致某种延迟发生(因此会跳过帧)。我还认为这些行应该围绕update()
,draw()
和PlayerCharacter
的{{1}}和Obstacle
方法。
问题是,我还是专门针对Android开发和Android 游戏开发的新手,所以我不知道是什么原因导致这种延迟。
我试着在网上寻找答案......不幸的是,我发现所有这些都指向了GC。但是,我不相信这种情况(如果我错了,请纠正我)这些答案对我不适用。我还阅读了android开发人员的MainGameBoard
页面,但找不到任何帮助。
所以,请帮助我找到解决这些令人讨厌的滞后的答案!
MainThread.java:
Performance Tips
MainGameBoard.java:
public class MainThread extends Thread {
public static final String TAG = MainThread.class.getSimpleName();
private final static int MAX_FPS = 60; // desired fps
private final static int MAX_FRAME_SKIPS = 5; // maximum number of frames to be skipped
private final static int FRAME_PERIOD = 1000 / MAX_FPS; // the frame period
private boolean running;
public void setRunning(boolean running) {
this.running = running;
}
private SurfaceHolder mSurfaceHolder;
private MainGameBoard mMainGameBoard;
public MainThread(SurfaceHolder surfaceHolder, MainGameBoard gameBoard) {
super();
mSurfaceHolder = surfaceHolder;
mMainGameBoard = gameBoard;
}
@Override
public void run() {
Canvas mCanvas;
Log.d(TAG, "Starting game loop");
long beginTime; // the time when the cycle begun
long timeDiff; // the time it took for the cycle to execute
int sleepTime; // ms to sleep (<0 if we're behind)
int framesSkipped; // number of frames being skipped
sleepTime = 0;
while(running) {
mCanvas = null;
try {
mCanvas = this.mSurfaceHolder.lockCanvas();
synchronized (mSurfaceHolder) {
beginTime = System.currentTimeMillis();
framesSkipped = 0;
this.mMainGameBoard.update();
this.mMainGameBoard.render(mCanvas);
timeDiff = System.currentTimeMillis() - beginTime;
sleepTime = (int) (FRAME_PERIOD - timeDiff);
if(sleepTime > 0) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {}
}
while(sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
// catch up - update w/o render
this.mMainGameBoard.update();
sleepTime += FRAME_PERIOD;
framesSkipped++;
}
}
} finally {
if(mCanvas != null)
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}
PlayerCharacter.java:
public class MainGameBoard extends SurfaceView implements
SurfaceHolder.Callback {
private MainThread mThread;
private PlayerCharacter mPlayer;
private Obstacle[] mObstacleArray = new Obstacle[4];
public static final String TAG = MainGameBoard.class.getSimpleName();
private long width, height;
private boolean gameStartedFlag = false, gameOver = false, update = true;
private Paint textPaint = new Paint();
private int scoreCount = 0;
private Obstacle collidedObs;
public MainGameBoard(Context context) {
super(context);
getHolder().addCallback(this);
DisplayMetrics displaymetrics = new DisplayMetrics();
((Activity) getContext()).getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
height = displaymetrics.heightPixels;
width = displaymetrics.widthPixels;
mPlayer = new PlayerCharacter(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher), width/2, height/2);
for (int i = 1; i <= 4; i++) {
mObstacleArray[i-1] = new Obstacle(width*(i+1) - 200, height, i);
}
mThread = new MainThread(getHolder(), this);
setFocusable(true);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mThread.setRunning(true);
mThread.start();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "Surface is being destroyed");
// tell the thread to shut down and wait for it to finish
// this is a clean shutdown
boolean retry = true;
while (retry) {
try {
mThread.join();
retry = false;
} catch (InterruptedException e) {
// try again shutting down the thread
}
}
Log.d(TAG, "Thread was shut down cleanly");
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN) {
if(update && !gameOver) {
if(gameStartedFlag) {
mPlayer.cancelJump();
mPlayer.setJumping(true);
}
if(!gameStartedFlag)
gameStartedFlag = true;
}
}
return true;
}
@SuppressLint("WrongCall")
public void render(Canvas canvas) {
onDraw(canvas);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.GRAY);
mPlayer.draw(canvas);
for (Obstacle obs : mObstacleArray) {
obs.draw(canvas);
}
if(gameStartedFlag) {
textPaint.reset();
textPaint.setColor(Color.WHITE);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(100);
canvas.drawText(String.valueOf(scoreCount), width/2, 400, textPaint);
}
if(!gameStartedFlag && !gameOver) {
textPaint.reset();
textPaint.setColor(Color.WHITE);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(72);
canvas.drawText("Tap to start", width/2, 200, textPaint);
}
if(gameOver) {
textPaint.reset();
textPaint.setColor(Color.WHITE);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(86);
canvas.drawText("GAME OVER", width/2, 200, textPaint);
}
}
public void update() {
if(gameStartedFlag && !gameOver) {
for (Obstacle obs : mObstacleArray) {
if(update) {
if(obs.isColidingWith(mPlayer)) {
collidedObs = obs;
update = false;
gameOver = true;
return;
} else {
obs.update(width);
if(obs.isScore(mPlayer))
scoreCount++;
}
}
}
if(!mPlayer.update() || !update)
gameOver = true;
}
}
}
Obstacle.java:
public void draw(Canvas canvas) {
canvas.drawBitmap(mBitmap, (float) x - (mBitmap.getWidth() / 2), (float) y - (mBitmap.getHeight() / 2), null);
}
public boolean update() {
if(jumping) {
y -= jumpSpeed;
jumpSpeed -= startJumpSpd/20f;
jumpTick--;
} else if(!jumping) {
if(getBottomY() >= startY*2)
return false;
y += speed;
speed += startSpd/25f;
}
if(jumpTick == 0) {
jumping = false;
cancelJump(); //rename
}
return true;
}
public void cancelJump() { //also called when the user touches the screen in order to stop a jump and start a new jump
jumpTick = 20;
speed = Math.abs(jumpSpeed);
jumpSpeed = 20f;
}
答案 0 :(得分:20)
答案 1 :(得分:6)
如果没有在Android上制作游戏,我确实使用Canvas和bufferStrategy在Java / AWT中制作了2D游戏......
如果您遇到闪烁,您可以通过渲染到屏幕外的图像,然后直接使用新的预渲染内容进行翻页/绘图,来获取手动双缓冲(摆脱闪烁)。
但是,我觉得你更关心动画中的“平滑度”,在这种情况下,我建议你在不同的动画刻度之间用插值扩展你的代码;
目前,渲染循环以与渲染相同的速度更新逻辑状态(逻辑移动),并使用一些参考时间进行测量并尝试跟踪传递的时间。
相反,您应该以您认为代码中“逻辑”所需的频率更新 - 通常10或25Hz就好了(我称之为“更新刻度”,这与实际完全不同FPS),而渲染是通过保持高分辨率的时间跟踪来测量你的实际渲染“需要多长时间”(我已经使用过nanoTime并且已经足够了,而currentTimeInMillis相当无用......),< / p>
通过这种方式,您可以在刻度线之间进行插值并渲染尽可能多的帧,直到下一次刻度,根据自上次刻度后经过的时间计算细粒度位置,与“应该”的时间相比较在两个刻度之间(因为你总是知道你在哪里 - 位置,以及你前进的位置 - 速度)
这样,无论CPU /平台如何,您都将获得相同的“动画速度”,但或多或少的平滑度,因为更快的CPU将在不同的刻度之间执行更多渲染。
一些复制粘贴/概念代码 - 但要注意这是AWT和J2SE,没有Android。然而,作为一个概念和一些Android化,我确信这种方法应该顺利呈现,除非你的逻辑/更新中的微积分太重(例如,用于碰撞检测的N ^ 2算法和N随着粒子系统等而变大) )。
有效渲染循环
而不是依赖重绘为你做绘画(可能需要不同的时间,取决于操作系统正在做什么),第一步是主动控制渲染循环并使用BufferStrategy渲染,然后在你完成之后,主动“显示”内容,然后再回到它。
缓冲策略
可能需要som特殊的Android内容才能开始,但这是相当直接的。我使用2页的bufferStrategy来创建一个“页面翻转”机制。
try
{
EventQueue.invokeAndWait(new Runnable() {
public void run()
{
canvas.createBufferStrategy(2);
}
});
}
catch(Exception x)
{
//BufferStrategy creation interrupted!
}
主要动画循环
然后,在你的主循环中,获取策略并采取主动控制(不要使用重绘)!
long previousTime = 0L;
long passedTime = 0L;
BufferStrategy strategy = canvas.getBufferStrategy();
while(...)
{
Graphics2D bufferGraphics = (Graphics2D)strategy.getDrawGraphics();
//Ensure that the bufferStrategy is there..., else abort loop!
if(strategy.contentsLost())
break;
//Calc interpolation value as a double value in the range [0.0 ... 1.0]
double interpolation = (double)passedTime / (double)desiredInterval;
//1:st -- interpolate all objects and let them calc new positions
interpolateObjects(interpolation);
//2:nd -- render all objects
renderObjects(bufferGraphics);
//Update knowledge of elapsed time
long time = System.nanoTime();
passedTime += time - previousTime;
previousTime = time;
//Let others work for a while...
Thread.yield();
strategy.show();
bufferGraphics.dispose();
//Is it time for an animation update?
if(passedTime > desiredInterval)
{
//Update all objects with new "real" positions, collision detection, etc...
animateObjects();
//Consume slack...
for(; passedTime > desiredInterval; passedTime -= desiredInterval);
}
}
管理的对象是上面的主循环,然后看起来像是一行;
public abstract class GfxObject
{
//Where you were
private GfxPoint oldCurrentPosition;
//Current position (where you are right now, logically)
protected GfxPoint currentPosition;
//Last known interpolated postion (
private GfxPoint interpolatedPosition;
//You're heading somewhere?
protected GfxPoint velocity;
//Gravity might affect as well...?
protected GfxPoint gravity;
public GfxObject(...)
{
...
}
public GfxPoint getInterpolatedPosition()
{
return this.interpolatedPosition;
}
//Time to move the object, taking velocity and gravity into consideration
public void moveObject()
{
velocity.add(gravity);
oldCurrentPosition.set(currentPosition);
currentPosition.add(velocity);
}
//Abstract method forcing subclasses to define their own actual appearance, using "getInterpolatedPosition" to get the object's current position for rendering smoothly...
public abstract void renderObject(Graphics2D graphics, ...);
public void animateObject()
{
//Well, move as default -- subclasses can then extend this behavior and add collision detection etc depending on need
moveObject();
}
public void interpolatePosition(double interpolation)
{
interpolatedPosition.set(
(currentPosition.x - oldCurrentPosition.x) * interpolation + oldCurrentPosition.x,
(currentPosition.y - oldCurrentPosition.y) * interpolation + oldCurrentPosition.y);
}
}
使用具有双精度的GfxPoint实用程序类来管理所有2D位置(因为插值运动可能非常精细,并且在渲染实际图形之前通常不需要舍入)。为了简化所需的数学内容并使代码更具可读性,我还添加了各种方法。
public class GfxPoint
{
public double x;
public double y;
public GfxPoint()
{
x = 0.0;
y = 0.0;
}
public GfxPoint(double init_x, double init_y)
{
x = init_x;
y = init_y;
}
public void add(GfxPoint p)
{
x += p.x;
y += p.y;
}
public void add(double x_inc, double y_inc)
{
x += x_inc;
y += y_inc;
}
public void sub(GfxPoint p)
{
x -= p.x;
y -= p.y;
}
public void sub(double x_dec, double y_dec)
{
x -= x_dec;
y -= y_dec;
}
public void set(GfxPoint p)
{
x = p.x;
y = p.y;
}
public void set(double x_new, double y_new)
{
x = x_new;
y = y_new;
}
public void mult(GfxPoint p)
{
x *= p.x;
y *= p.y;
}
public void mult(double x_mult, double y_mult)
{
x *= x_mult;
y *= y_mult;
}
public void mult(double factor)
{
x *= factor;
y *= factor;
}
public void reset()
{
x = 0.0D;
y = 0.0D;
}
public double length()
{
double quadDistance = x * x + y * y;
if(quadDistance != 0.0D)
return Math.sqrt(quadDistance);
else
return 0.0D;
}
public double scalarProduct(GfxPoint p)
{
return scalarProduct(p.x, p.y);
}
public double scalarProduct(double x_comp, double y_comp)
{
return x * x_comp + y * y_comp;
}
public static double crossProduct(GfxPoint p1, GfxPoint p2, GfxPoint p3)
{
return (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
}
public double getAngle()
{
double angle = 0.0D;
if(x > 0.0D)
angle = Math.atan(y / x);
else if(x < 0.0D)
angle = Math.PI + Math.atan(y / x);
else if(y > 0.0D)
angle = Math.PI / 2;
else
angle = - Math.PI / 2;
if(angle < 0.0D)
angle += 2 * Math.PI;
if(angle > 2 * Math.PI)
angle -= 2 * Math.PI;
return angle;
}
}
答案 2 :(得分:4)
尝试使用此尺寸。您会注意到您只在最短的时间内同步并锁定画布。否则操作系统将A)丢弃缓冲区,因为你太慢或B)完全不更新,直到你的睡眠等待结束。
public class MainThread extends Thread
{
public static final String TAG = MainThread.class.getSimpleName();
private final static int MAX_FPS = 60; // desired fps
private final static int MAX_FRAME_SKIPS = 5; // maximum number of frames to be skipped
private final static int FRAME_PERIOD = 1000 / MAX_FPS; // the frame period
private boolean running;
public void setRunning(boolean running) {
this.running = running;
}
private SurfaceHolder mSurfaceHolder;
private MainGameBoard mMainGameBoard;
public MainThread(SurfaceHolder surfaceHolder, MainGameBoard gameBoard) {
super();
mSurfaceHolder = surfaceHolder;
mMainGameBoard = gameBoard;
}
@Override
public void run()
{
Log.d(TAG, "Starting game loop");
long beginTime; // the time when the cycle begun
long timeDiff; // the time it took for the cycle to execute
int sleepTime; // ms to sleep (<0 if we're behind)
int framesSkipped; // number of frames being skipped
sleepTime = 0;
while(running)
{
beginTime = System.currentTimeMillis();
framesSkipped = 0;
synchronized(mSurfaceHolder){
Canvas canvas = null;
try{
canvas = mSurfaceHolder.lockCanvas();
mMainGameBoard.update();
mMainGameBoard.render(canvas);
}
finally{
if(canvas != null){
mSurfaceHolder.unlockCanvasAndPost(canvas);
}
}
}
timeDiff = System.currentTimeMillis() - beginTime;
sleepTime = (int)(FRAME_PERIOD - timeDiff);
if(sleepTime > 0){
try{
Thread.sleep(sleepTime);
}
catch(InterruptedException e){
//
}
}
while(sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
// catch up - update w/o render
mMainGameBoard.update();
sleepTime += FRAME_PERIOD;
framesSkipped++;
}
}
}
}
答案 3 :(得分:1)
首先,Canvas表现不佳,所以不要期望太多。您可能想尝试SDK中的lunarlander示例,看看您在硬件上获得的性能。
尝试将最大fps降低到30以下,目标是平稳不快。
private final static int MAX_FPS = 30; // desired fps
还可以摆脱睡眠调用,渲染到画布可能会睡不够。尝试更像:
synchronized (mSurfaceHolder) {
beginTime = System.currentTimeMillis();
framesSkipped = 0;
timeDiff = System.currentTimeMillis() - beginTime;
sleepTime = (int) (FRAME_PERIOD - timeDiff);
if(sleepTime <= 0) {
this.mMainGameBoard.update();
this.mMainGameBoard.render(mCanvas);
}
}
如果您愿意,可以比渲染更频繁地完成this.mMainGameBoard.update()
。
编辑:此外,因为你说障碍出现时事情会变慢。尝试将它们绘制到屏幕外的Canvas / Bitmap。我听说有些drawSHAPE方法是经过CPU优化的,你可以更好地将它们绘制到离线画布/位图,因为它们不是硬件/ gpu加速的。
Edit2:Canvas.isHardwareAccelerated()为您返回什么?
答案 4 :(得分:0)
游戏中减速和口吃的最常见原因之一是图形管道。游戏逻辑的处理速度比绘制(通常)快得多,因此您需要确保以最有效的方式绘制所有内容。您可以在下面找到有关如何实现此目标的一些提示。
一些建议使其更好