机器人游戏中令人讨厌的滞后/口吃编辑

我刚刚开始在Android中进行游戏开发,我正在开发一个超级简单的游戏。

游戏基本上就像飞来飞去的鸟。

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

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

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

延迟主要发生在角色通过障碍物时(有时也会在障碍物之后)。发生的事情是,游戏状态的每次绘制都有不均匀的延迟,导致运动中的卡顿。

  • GC 不会根据日志运行。
  • 口吃不是由速度太高引起的(我知道,因为在游戏开始时,当障碍物不在视线范围内时,角色移动平稳)
  • 我不认为问题也与FPS有关,因为即使MAX_FPS字段设置为100,仍然有口吃。

我的想法是,有一行或多行代码会导致某种延迟发生(因此跳过帧)。我还认为这些线应该围绕 和 的方法、 、 和 。update()draw()PlayerCharacterObstacleMainGameBoard

问题是,我对Android开发和Android游戏开发还很陌生,所以我不知道是什么原因会导致这样的延迟。

我试着在网上寻找答案...不幸的是,我发现所有这些都指向GC是罪魁祸首。但是,我不相信这种情况(如果我错了,请纠正我),这些答案不适用于我。我还阅读了Android开发人员的页面,但找不到任何有帮助的东西。Performance Tips

所以,请帮我找到解决这些烦人的滞后的答案!

一些代码

主线程.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);
        }
    }
}
}

主游戏板.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;
    }
}

}

玩家角色.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;
}

障碍.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;
}

答案 1

更新:尽管这很详细,但它几乎没有触及表面。现在提供了更详细的解释。游戏循环建议在附录 A 中。如果你真的想了解发生了什么,那就从这里开始。

原始帖子如下...


我将从Android中图形管道如何工作的胶囊摘要开始。你可以找到更彻底的治疗方法(例如,一些非常详细的Google I / O谈话),所以我只是达到了高点。结果比我预期的要长得多,但我一直想写一些这样的文章。

表面抛锚器

您的应用程序不会在帧缓冲器上绘制。有些设备甚至没有帧缓冲器。您的应用程序拥有对象的“生产者”端。当它完成渲染帧时,它会调用 或 ,这会将完成的缓冲区排队以进行显示。(从技术上讲,渲染甚至可能不会开始,直到您告诉它交换,并且可以在缓冲区通过管道时继续,但这是另一个故事。BufferQueueunlockCanvasAndPost()eglSwapBuffers()

缓冲区被发送到队列的“使用者”端,在本例中是 SurfaceFlinger,即系统表面复合器。缓冲区通过句柄传递;不会复制内容。每次显示刷新(我们称之为“VSYNC”)开始时,SurfaceFlinger都会查看所有各种队列,以查看哪些缓冲区可用。如果找到新内容,它将从该队列中闩锁下一个缓冲区。如果没有,它将使用以前获得的任何内容。

然后,将具有可见内容的窗口(或“图层”)的集合组合在一起。这可以通过SurfaceFlinger(使用OpenGL ES将层渲染到新的缓冲区中)或通过硬件编写器HAL来完成。硬件编写器(在最新设备上可用)由硬件 OEM 提供,可以提供许多“覆盖”平面。如果 SurfaceFlinger 有三个要显示的窗口,并且 HWC 有三个可用的叠加平面,它会将每个窗口放入一个叠加层中,并在显示帧时执行合成。从来没有一个缓冲区可以保存所有数据。这通常比在GLES中做同样的事情更有效。(顺便说一句,这就是为什么您无法通过简单地打开帧缓冲器开发条目并读取像素来获取最新设备上的屏幕截图的原因。

这就是消费者方面的样子。您可以自己欣赏它。让我们回到生产者(即你的应用程序)。adb shell dumpsys SurfaceFlinger

制片人

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

曲面的缓冲区队列是三重缓冲的。这意味着你可以扫描一个缓冲区以用于显示,一个缓冲区位于 SurfaceFlinger 等待下一个 VSYNC,以及一个缓冲区供应用使用。拥有更多缓冲区可提高吞吐量并消除颠簸,但会增加触摸屏幕和看到更新之间的延迟。在此基础上添加整个帧的额外缓冲通常不会对您有太大好处。SurfaceView

如果绘制速度超过显示器渲染帧的速度,则最终将填满队列,并且缓冲区交换调用 () 将暂停。这是使游戏的更新速率与显示速率相同的简单方法 - 尽可能快地绘制,并让系统减慢您的速度。每一帧,您根据经过的时间进行前进状态。(我在Android Breakout中使用了这种方法。这不太对,但是在60fps下,您不会真正注意到缺陷。如果你睡眠时间不够长,你也会得到同样的电话效果 - 你醒来后只会等待队列。在这种情况下,睡眠没有任何优势,因为在队列上睡眠同样有效。unlockCanvasAndPost()sleep()

如果绘制速度低于显示可以渲染的帧,则队列最终将运行干,并且 SurfaceFlinger 将在两次连续的显示刷新中显示相同的帧。如果您尝试通过呼叫来调整游戏速度,并且睡眠时间过长,则会定期发生这种情况。由于理论原因(如果没有反馈机制,很难实现PLL)和实际原因(刷新率会随时间而变化,例如,我在给定设备上看到它从58fps到62fps不等),因此不可能精确匹配显示器刷新率。sleep()

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

不睡觉

您有几种选择。您可以使用“在缓冲区交换调用备份之前尽可能快地绘制”方法,这是许多基于应用程序执行的操作(无论他们是否知道)。或者你可以使用编舞。GLSurfaceView#onDraw()

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

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

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

您可以在Breakout中看到这一点,它根据经过的时间推进球的位置。它将时间上的大跳跃切割成更小的部分,以保持碰撞检测的简单性。Breakout的问题在于它使用的是填充队列完整方法,时间戳受SurfaceFlinger完成工作所需时间的变化的影响。此外,当缓冲区队列最初为空时,您可以非常快速地提交帧。(这意味着您计算两个时间增量几乎为零的帧,但它们仍以60fps的速度发送到显示器。在实践中,您看不到这一点,因为时间戳差异非常小,以至于它看起来像绘制了两次的同一帧,并且仅在从非动画过渡到动画时发生,因此您看不到任何卡顿。

使用编舞者,您可以获得实际的VSYNC时间,因此您可以获得一个不错的常规时钟来建立时间间隔。由于您使用显示器刷新时间作为时钟源,因此您永远不会与显示器不同步。

当然,您仍然必须准备好放弃帧。

没有留下任何框架

不久前,我在Grafika(“Record GL应用程序”)中添加了一个屏幕录制演示,该演示可以执行非常简单的动画 - 只是一个平面阴影的弹跳矩形和一个旋转的三角形。它前进状态并在编舞者发出信号时绘制。我编码了它,运行了它...并开始注意到编舞者的回调正在备份。

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

GL渲染几乎是即时的,但是编舞者更新被传递到UI线程,UI线程正在对布局产生影响,所以我的渲染器线程直到很久以后才得到信号。(你可以让编舞者将信号直接传送到渲染器线程,但是编舞者中有一个错误,如果你这样做,会导致内存泄漏。修复方法是在当前时间比 VSYNC 时间晚 15 毫秒以上时丢弃帧。该应用程序仍然执行状态更新 - 碰撞检测是如此基本,如果你让时间差距变得太大,就会发生奇怪的事情 - 但它不会向SurfaceFlinger提交缓冲区。

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

主要经验教训:

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

绘图

如果画布是硬件加速的,则渲染到画布可能会非常高效。如果不是这样,而你是在软件中绘图的,这可能需要一段时间 - 特别是如果你触摸了很多像素。

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

您还可以通过直接使用OpenGL ES来消除渲染中的一些神秘感。学习事情是如何运作的,有一些颠簸,但Breakout(以及一个更详细的例子,Replica Island)展示了简单游戏所需的一切。


答案 2

我从来没有在Android上制作过游戏,我确实使用Canvas和bufferStrategy在Java / AWT中制作了2D游戏......

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

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

目前,渲染循环以与渲染相同的速度更新逻辑状态(以逻辑方式移动内容),并使用一些参考时间来测量并尝试跟踪经过的时间。

相反,你应该以你认为适合代码中的“逻辑”工作的任何频率进行更新 - 通常10或25Hz就可以了(我称之为“更新刻度”,这与实际的FPS完全不同),而渲染是通过保持高分辨率的时间跟踪来完成的,以测量实际渲染需要“多长时间”(我已经使用了nanoTime,这已经足够了, 而currentTimeInMillis是相当无用的...),

通过这种方式,您可以在逐笔报价之间进行插值,并通过根据自上次分时以来经过的时间计算细粒度位置,而不是在两个逐笔报价之间“应该”花费的时间(因为您始终知道您在哪里 - 位置,以及您前进的方向 - 速度)来计算尽可能多的帧,直到下一个价格变动。

这样,无论CPU/平台如何,您都将获得相同的“动画速度”,但或多或少的平滑度,因为更快的CPU将在不同的价格变动之间执行更多的渲染。

编辑

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

活动渲染循环

第一步不是依靠重绘来为您完成绘画(这可能需要不同的时间,具体取决于操作系统正在做什么),而是主动控制渲染循环并使用BufferStrategy进行渲染,然后在完成后主动“显示”内容,然后再返回。

缓冲区策略

可能需要特殊的Android东西才能开始,但它相当简单。我使用2页的缓冲区策略来创建“翻页”机制。

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);
    }
}

所有 2D 位置都使用具有双精度的 GfxPoint 实用程序类进行管理(因为插值的移动可能非常精细,并且在渲染实际图形之前通常不需要舍入)。为了简化所需的数学内容并使代码更具可读性,我还添加了各种方法。

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

推荐