在安卓游戏中令人讨厌的滞后/口吃

Asa*_*saf 18 java android surfaceview game-loop flappy-bird-clone

我刚开始在android中进行游戏开发,而我正在开发一款超级简单的游戏.

这场比赛基本上就像飞扬的小鸟.

我设法让一切工作,但我得到了很多口吃和滞后.

我用来测试的手机是LG G2,所以它应该并且确实运行的游戏比这更重,更复杂.

基本上有4个"障碍"是全屏宽度彼此分开.
当游戏开始时,障碍物开始以恒定速度移动(朝向角色).玩家角色的x值在整个游戏中是一致的,而其y值会发生变化.

滞后主要发生在角色穿过障碍物时(有时也会在障碍物之后).会发生的是,游戏状态的每个图形都存在不均匀的延迟,导致运动中出现断断续续的情况.

  • GC不会根据日志运行.
  • 口吃不是由于速度太高造成的(我知道因为在游戏开始时障碍物不在视线范围内,角色移动得很顺利)
  • 我不认为问题也与FPS有关,因为即使将MAX_FPS字段设置为100,仍然会出现断断续续的情况.

我的想法是有一行或多行代码导致某种延迟发生(因此跳过帧).我也认为这些线路应该是围绕update()draw()方法PlayerCharacter,Obstacle以及MainGameBoard.

问题是,我还是专门针对Android开发和Android 游戏开发的新手,所以我不知道是什么原因导致这种延迟.

我试着在网上寻找答案......不幸的是,我发现所有这些都指向了GC.但是,我不相信这种情况(如果我错了,请纠正我)这些答案对我不适用.我也阅读了android开发者的Performance Tips页面,但找不到任何帮助.

所以,请帮助我找到解决这些令人讨厌的滞后的答案!

一些代码

MainThread.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);
        }
    }
}
}
Run Code Online (Sandbox Code Playgroud)

MainGameBoard.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;
    }
}

}
Run Code Online (Sandbox Code Playgroud)

PlayerCharacter.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;
}
Run Code Online (Sandbox Code Playgroud)

Obstacle.java:

public void draw(Canvas canvas) {
    Paint pnt = new Paint();
    pnt.setColor(Color.CYAN);
    canvas.drawRect(x, 0, x+200, ySpaceStart, pnt);
    canvas.drawRect(x, ySpaceStart+500, x+200, y, pnt);
    pnt.setColor(Color.RED);
    canvas.drawCircle(x, y, 20f, pnt);
}

public void update(long width) {
    x -= speed;

    if(x+200 <= 0) {
        x = ((startX+200)/(index+1))*4 - 200;
        ySpaceStart = r.nextInt((int) (y-750-250+1)) + 250;
        scoreGiven = false;
    }
}

public boolean isColidingWith(PlayerCharacter mPlayer) {
    if(mPlayer.getRightX() >= x && mPlayer.getLeftX() <= x+20)
        if(mPlayer.getTopY() <= ySpaceStart || mPlayer.getBottomY() >= ySpaceStart+500)
            return true;

    return false;
}

public boolean isScore(PlayerCharacter mPlayer) {
    if(mPlayer.getRightX() >= x+100 && !scoreGiven) {
        scoreGiven = true;
        return true;
    }

    return false;
}
Run Code Online (Sandbox Code Playgroud)

fad*_*den 20

更新:就像这样详细,它几乎没有触及表面.现在可以获得更详细的解释.游戏循环建议在附录A中.如果您真的想了解正在发生的事情,请从此开始.

原帖如下......


我将首先介绍Android中图形管道的工作原理.你可以找到更全面的治疗方法(例如一些非常详细的谷歌I/O会谈),所以我只是达到了高点.结果比我预期的要长,但我一直想写一些这样的东西.

SurfaceFlinger的

您的应用程序不会在Framebuffer上绘制.有些设备甚至没有 Framebuffer.您的应用程序保存对象的"生产者"端BufferQueue.当它完成渲染帧时,它会调用unlockCanvasAndPost()或者eglSwapBuffers()将完成的缓冲区排队等待显示.(从技术上讲,渲染可能甚至没有开始,直到你告诉它交换并且可以在缓冲区移动通过管道时继续,但这是另一个时间的故事.)

缓冲区被发送到队列的"消费者"端,在这种情况下是系统表面合成器SurfaceFlinger.缓冲区通过句柄传递; 内容不会被复制.每次显示刷新(让我们称之为"VSYNC")开始时,SurfaceFlinger会查看所有各种队列以查看可用的缓冲区.如果找到新内容,则会锁定该队列中的下一个缓冲区.如果没有,它会使用之前获得的任何内容.

然后将具有可见内容的窗口(或"层")的集合合成在一起.这可以通过SurfaceFlinger(使用OpenGL ES将图层渲染到新缓冲区)或通过Hardware Composer HAL完成.硬件组合器(在最新设备上可用)由硬件OEM提供,并且可以提供许多"覆盖"平面.如果SurfaceFlinger有三个要显示的窗口,并且HWC有三个可用的覆盖平面,它会将每个窗口放入一个叠加层,并在显示帧时进行合成.从来没有一个缓冲区可以容纳所有数据.这通常比在GLES中做同样的事情更有效.(顺便说一句,这就是为什么你不能通过打开framebuffer dev条目和读取像素来获取最近设备上的屏幕截图.)

这就是消费者方面的样子.你可以自己欣赏它adb shell dumpsys SurfaceFlinger.让我们回到制作人(即你的应用).

制片人

您使用的是a SurfaceView,它有两个部分:一个与系统UI一起使用的透明视图,以及一个独立的Surface层.它SurfaceView的表面直接进入SurfaceFlinger,这就是为什么它比其他方法(如TextureView)具有更少的开销.

SurfaceView表面的BufferQueue 是三重缓冲的.这意味着你可以扫描出一个缓冲区用于显示,一个缓冲区位于SurfaceFlinger,等待下一个VSYNC,一个缓冲区供应用程序绘制.拥有更多缓冲区可提高吞吐量并消除颠簸,但会增加触摸屏幕与查看更新之间的延迟.在此基础上添加额外的整个帧缓冲通常不会对你有好处.

如果绘制的速度比显示器渲染帧快,则最终会填满队列,缓冲区交换调用(unlockCanvasAndPost())将暂停.这是让游戏的更新速率与显示速率相同的简单方法 - 尽可能快地绘制,让系统降低速度.每一帧,你根据经过的时间提前状态.(我在Android Breakout中使用了这种方法.)这不太对,但是在60fps时你不会真正注意到这些不完美之处.sleep()如果你没有长时间睡眠,你会得到与呼叫相同的效果- 你只会在队列中等待醒来.在这种情况下,睡眠没有任何好处,因为在队列中睡觉同样有效.

如果绘制的速度慢于显示器可以渲染帧,则队列最终会变干,SurfaceFlinger将在两次连续的显示刷新时显示相同的帧.如果您试图通过sleep()电话调整游戏速度并且睡眠时间过长,这将会定期发生.这是不可能精确匹配的显示刷新速率,理论上的原因(这是很难实现的PLL没有反馈机制)和现实的原因(刷新率可以随时间而改变,例如,我已经看到了从58fps的不同而不同,以62fps给定的设备).

sleep()在游戏循环中使用调用来调整动画速度是一个坏主意.

没睡觉

你有几个选择.您可以使用"尽可能快地绘制,直到缓冲区交换调用备份"方法,这是许多应用程序所依据的GLSurfaceView#onDraw()(无论他们是否知道).或者你可以使用Choreographer.

编舞器允许您设置在下一个VSYNC上触发的回调.重要的是,回调的参数是实际的VSYNC时间.因此,即使您的应用程序没有立即唤醒,您仍然可以准确了解显示屏刷新的时间.在更新游戏状态时,这非常有用.

更新游戏状态的代码永远不应该被设计为推进"一帧".鉴于设备种类繁多,以及单个设备可以使用的各种刷新率,您无法知道"帧"是什么.你的游戏会稍微慢一点或稍微快一点 - 或者如果你运气好,有人试图通过HDMI锁定到48Hz的电视上播放它,你将会非常迟钝.您需要确定前一帧和当前帧之间的时差,并适当地提前游戏状态.

这可能需要一些精神上的重新洗牌,但这是值得的.

你可以在Breakout中看到这个,它根据经过的时间推进球的位置.它可以及时将大跳跃切成小块,以保持碰撞检测的简单性.Breakout的问题在于它使用了stuff-the-queue-full方法,时间戳可能会受到SurfaceFlinger工作所需时间的影响.此外,当缓冲区队列最初为空时,您可以非常快速地提交帧.(这意味着你计算两个时间差几乎为零的帧,但它们仍然以60fps的速度发送到显示器.实际上你没有看到这个,因为时间戳差异很小,看起来就像是同一帧两次绘制,只有在从非动画转换到动画时才会发生,所以你看不到任何口吃.)

使用Choreographer,您可以获得实际的VSYNC时间,因此您可以获得一个很好的常规时钟,以使您的时间间隔为基础.因为您使用显示刷新时间作为时钟源,所以您永远不会与显示器不同步.

当然,你仍然需要准备丢帧.

没有留下任何框架

前段时间我向Grafika("记录GL应用程序")添加了一个屏幕录制演示,它做了非常简单的动画 - 只是一个平面阴影的弹跳矩形和一个旋转的三角形.当编舞者发出信号时,它会提升状态并绘制.我编写了它,运行它...并开始注意到Choreographer回调备份.

在使用systrace进行挖掘之后,我发现框架UI偶尔会进行一些布局工作(可能与UI层中的按钮和文本有关,它位于SurfaceView表面的顶部).通常这需要6ms,但如果我没有主动在屏幕上移动手指,我的Nexus 5会减慢各种时钟的速度以降低功耗并延长电池寿命.重新布局需要28ms.请记住,60fps帧是16.7ms.

GL渲染几乎是即时的,但是Choreographer更新被传递到UI线程,它正在磨损布局,所以我的渲染器线程直到很久以后才得到信号.(你可以让Choreographer直接将信号传递给渲染器线程,但是如果你这样做,Choreographer中会出现一个会导致内存泄漏的错误.)修复是在当前时间超过VSYNC时间后的15ms时丢帧.该应用程序仍然进行状态更新 - 碰撞检测非常简陋,如果你让时间差距变得太大,就会发生奇怪的事情 - 但是它没有向SurfaceFlinger提交缓冲区.

在运行应用程序时,您可以判断帧何时被丢弃,因为Grafika将边框闪烁红色并更新屏幕上的计数器.通过观看动画你无法分辨.因为状态更新是基于时间间隔而不是帧计数,所以一切都移动的速度与帧丢失的速度一样快,并且在60fps时您不会注意到单个丢帧.(在某种程度上取决于你的眼睛,游戏和显示硬件的特性.)

主要课程:

  • 帧丢失可能是由外部因素引起的 - 依赖于另一个线程,CPU时钟速度,后台gmail同步等.
  • 你无法避免所有的帧丢失.
  • 如果你正确设置绘制循环,没有人会注意到.

画画

如果硬件加速,渲染到画布可能非常有效.如果不是,并且您正在使用软件进行绘图,则可能需要一段时间 - 特别是如果您触摸了大量像素.

阅读的两个重要部分:了解硬件加速渲染,并使用硬件缩放器减少应用需要触摸的像素数.Grafika中的"硬件缩放器练习器"将让您了解在缩小绘图表面大小时会发生什么 - 在效果明显之前,您可以获得相当小的效果.(我发现在100x64表面上观看GL渲染旋转三角形时,我觉得很奇怪.)

您还可以直接使用OpenGL ES来渲染渲染中的一些谜团.有一点可以了解事情是如何运作的,但是Breakout(以及更详细的例子,Replica Island)显示了简单游戏所需的一切.


Mar*_*ord 6

在没有用Android制作游戏的情况下,我确实使用Canvas和bufferStrategy在Java/AWT中制作了2D游戏......

如果您遇到闪烁,您可以通过渲染到屏幕外的图像,然后直接使用新的预渲染内容进行翻页翻转/绘图图像来寻找手动双缓冲区(摆脱闪烁).

但是,我觉得你更关心动画中的"平滑度",在这种情况下,我建议你在不同的动画刻度之间用插值扩展你的代码;

目前,渲染循环以与渲染相同的速度更新逻辑状态(逻辑移动),并使用一些参考时间进行测量并尝试跟踪传递的时间.

相反,您应该以您认为代码中"逻辑"所需的频率更新 - 通常10或25Hz就好了(我称之为"更新刻度",这与实际FPS完全不同),而渲染是通过保持高分辨率的时间跟踪来测量你的实际渲染所需的"多长时间"(我已经使用过nanoTime并且已经足够了,而currentTimeInMillis则相当无用......),

通过这种方式,您可以在刻度线之间进行插值并渲染尽可能多的帧,直到下一次刻度为止,根据自上次刻度后经过的时间计算细粒度位置,与"应该"在两次之间的时间相比.滴答声(因为你总是知道你在哪里 - 位置,以及你前进的位置 - 速度)

这样,无论CPU /平台如何,您都将获得相同的"动画速度",但或多或​​少具有平滑性,因为更快的CPU将在不同的刻度之间执行更多渲染.

编辑

一些复制粘贴/概念代码 - 但要注意这是AWT和J2SE,没有Android.然而,作为一个概念和一些Android化,我确信这种方法应该顺利呈现,除非你的逻辑/更新中的微积分太重(例如,用于碰撞检测的N ^ 2算法和N随着粒子系统等而变大) ).

主动渲染循环

而不是依赖重绘为你做绘画(这可能需要不同的时间,取决于操作系统正在做什么),第一步是主动控制渲染循环并使用BufferStrategy在你渲染,然后积极"显示"你完成后的内容,然后再回到它.

缓冲策略

可能需要一些特殊的Android内容才能开始,但这是相当直接的.我使用2页的bufferStrategy来创建一个"页面翻转"机制.

try
{
     EventQueue.invokeAndWait(new Runnable() {
        public void run()
        {
            canvas.createBufferStrategy(2);
        }
    });
}    
catch(Exception x)
{
    //BufferStrategy creation interrupted!        
}
Run Code Online (Sandbox Code Playgroud)

主要动画循环

然后,在您的主循环中,获取策略并采取主动控制(不要使用重绘)!

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

一个被管理的对象就是上面的主循环,然后看起来就像是一条线;

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

使用具有双精度的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;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 嗨 - 上面用我在游戏"后面"中使用的代码/概念方法编辑了我的答案;)我使用10Hz进行动画(逻辑步骤/刻度),而渲染在60FPS时像丝绸一样光滑,我有一些严重碰撞检测/弹跳演算也在进行中.我敢说,这种方法很好,并且已被充分证明,插值作为一种概念在今天的所有"专业"游戏中被用于将逻辑位置与渲染位置分开,并在慢速和快速平台上实现相同的"游戏速度".祝好运 ;) (2认同)