控制二维游戏的动画速度

Controlling animation speed of a 2d game

我正在制作一个小型小行星游戏,但在控制动画速度方面遇到了一些问题。

例如,假设我的游戏中有 20 颗小行星,当我摧毁一颗小行星时,小行星的数量就会减少(很明显)。因为游戏中的物体变少了,fps上去了,小行星的动画速度也越来越快

我通过根据我在游戏中拥有的小行星的数量调整动画速度来修复它,但是当我摧毁小行星时我还面临另一个爆炸问题。我想我可以对小行星做同样的事情,但我只是认为这不是一个非常明智的“解决”它的方法,而且对我来说似乎是一种糟糕的做法。

我想过限制 fps,但我不太确定该怎么做。我想得到一些建议,以及处理这种情况的最佳方法是什么。

我将在此处 post 我的主要游戏 class 包括游戏循环和爆炸示例 class 这样您就可以大致了解代码。

游戏class和循环:

import com.asteroids.view.*;

public class Game extends Canvas implements Runnable {

private static final long serialVersionUID = -8921419424614180143L;
public static final int WIDTH = 1152, HEIGHT = WIDTH / 8 * 5;

private Thread thread;
private boolean isRunning;
private LoadImages loadImages = new LoadImages();
private Player player = new Player();
private AllObjects objects;
private KeyInput keyInput;
private long delay = 80;
private long currentTime = System.currentTimeMillis();
private long expectedTime = currentTime + delay;
public static BufferedImage test;

public Game() {
    new Window(WIDTH, HEIGHT, "Asteroids!", this);
    objects = new AllObjects();
    objects.addObject(player);
    for (int i = 0; i < 20; i++) {
        objects.addObject(new Rock((int) (Math.random() * (Game.WIDTH - 64) + 1),
                (int) (Math.random() * (Game.HEIGHT - 64) + 1)));
    }
    keyInput = new KeyInput(player);
    this.addKeyListener(keyInput);
}

public void run() {
    this.requestFocus();
    long lastTime = System.nanoTime();
    double amountOfTicks = 60.0;
    double ns = 1000000000 / amountOfTicks;
    double delta = 0;
    long timer = System.currentTimeMillis();
    int frames = 0;

    // main game loop.
    while (isRunning) {
        adjustAsteroidsSpeed();
        destroyAsteroids();
        collisionLoop();

        // used to set delay between every bullet(milliseconds)
        currentTime = System.currentTimeMillis();
        if (KeyInput.shoot && currentTime >= expectedTime) {

            // calculates the accurate position of the x,y on the "circumference" of the
            // player
            float matchedX = player.getX() + 1 + (float) ((player.getRadius() + 32) * Math.cos(player.getRadian()));
            float matchedY = player.getY() - 7 + (float) ((player.getRadius() + 32) * Math.sin(player.getRadian()));
            objects.addObject(new Bullet(matchedX, matchedY, player));
            expectedTime = currentTime + delay;
        }
        destroyBullets();
        long now = System.nanoTime();
        delta += (now - lastTime) / ns;
        lastTime = now;
        while (delta >= 1) {
            tick();
            delta--;
        }
        if (isRunning)
            render();
        frames++;
        if (System.currentTimeMillis() - timer > 1000) {
            timer += 1000;
            System.out.println("FPS: " + frames);
            frames = 0;
        }
    }

    render();

    stop();
    System.exit(1);

}

private void stop() {
    try {
        thread.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.exit(1);

}

private void render() {
    BufferStrategy bs = this.getBufferStrategy();
    if (bs == null) {
        this.createBufferStrategy(3);
        return;
    }

    Graphics g = bs.getDrawGraphics();
    g.drawImage(LoadImages.getbackground(), 0, 0, getWidth(), getHeight(), this);
    objects.render(g);
    player.render(g);
    g.dispose();
    bs.show();

}

private void tick() {
    player.tick();
    objects.tick();
}

// starting thread and game loop.
public void start() {
    thread = new Thread(this);
    thread.start();
    isRunning = true;
}

// minimum and maximum possible position for object.
public static float Bounds(float value, float min, float max) {
    if (value >= max) {
        return value = max;
    }
    if (value <= min) {
        return value = min;
    } else {
        return value;
    }

}

// detects collision between two objects
public boolean collision(GameObject a, GameObject b) {
    return (b.getX() - a.getX() + 10) * (b.getX() - a.getX() + 10)
            + (b.getY() - a.getY() + 10) * (b.getY() - a.getY() + 10) < (a.getRadius() + b.getRadius())
                    * (a.getRadius() + b.getRadius());
}

// destroys bullets once they go out of the screen
public void destroyBullets() {
    for (int i = 0; i < objects.getSize(); i++) {
        if (objects.get(i).getId() == ID.BULLET) {
            GameObject bullet = objects.get(i);
            if (bullet.getX() > Game.WIDTH || bullet.getX() < 0 || bullet.getY() > Game.HEIGHT
                    || bullet.getY() < 0) {
                objects.removeObject(bullet);
            }
        }
    }
}

// whenever a collision between an asteroid and a bullet occurs, the asteroid and the bullets are destroyed
public void destroyAsteroids() {
    GameObject bullet = null;
    GameObject bigRock = null;
    for (int i = 0; i < objects.getSize(); i++) {
        if (objects.get(i).getId() == ID.BULLET) {
            bullet = (Bullet) objects.get(i);
            for (int q = 0; q < objects.getSize(); q++) {
                if (objects.get(q).getId() == ID.BIGROCK) {
                    bigRock = objects.get(q);
                    if (bullet != null && bigRock != null) {
                        if (collision(bigRock, bullet)) {
                            objects.addObject(new Explosion(bigRock.getX(), bigRock.getY(), objects));
                            objects.removeObject(bigRock);
                            objects.removeObject(bullet);
                        }
                    }
                }
            }
        }
    }
}

// calculates the amount of asteroids in the game and adjust the asteroids speed
public void adjustAsteroidsSpeed() {
    int rocksCount = 0;
    Rock rock;
    for (GameObject object : objects.link()) {
        if (object.getId() == ID.BIGROCK) {
            rocksCount++;
        }
    }
    for (GameObject object : objects.link()) {
        if (object.getId() == ID.BIGROCK) {
            rock = (Rock) object;
            rock.setAnimSpeed(rocksCount * 0.002f);
        }
    }
 }

爆炸class:

package com.asteroids.model;

import java.awt.Graphics;
import java.awt.Image;

import com.asteroids.controller.*;
import com.asteroids.view.LoadImages;


public class Explosion extends GameObject {

private AllObjects objects;
private Image explosion;
private float frame = 0;
private float animSpeed = 0.09f;
private int frameCount = 48;

public Explosion(float x, float y, AllObjects objects) {
    super(x, y, ID.EXPLOSION, 1);
    this.objects = objects;
}

public void render(Graphics g) {
    explosion(g);
}

public void explosion(Graphics g) {
    frame += animSpeed;
    if (frame > frameCount) {
        frame -= frameCount;
    }
    explosion = LoadImages.getExplosion().getSubimage((int) frame * 256, 0, 256, 256);
    g.drawImage(explosion, (int) x, (int) y, 110, 110, null);
    if (frame >= 47.8f) {
        objects.removeObject(this);
    }
}

public void tick() {
    
}

public void setAnimSpeed(float animSpeed) {
    this.animSpeed = animSpeed;
}
}

您的主循环正在生成不均匀的更新。如果我什么都不做,我会达到 77999138284754 fps 之间的任何位置,但是,如果我投入 8 毫秒延迟(以模拟某些工作),它会下降到 [=15] 左右=]-120 fps.

您的意图是尝试让帧速率尽可能均匀,这将确保动画速度保持不变

就我个人而言,我不喜欢 "free-wheeling" 风格的游戏循环,这意味着允许循环消耗 CPU 个循环而不实际做任何事情,而这些循环可能是用来做更重要的工作,比如更新 UI。

在大多数情况下,我只是将 Swing Timer 设置为 5 毫秒间隔,然后使用 date/time API 来计算差异从现在到上次更新之间,并选择要做什么,但是,这假设您使用的是基于 Swing 的绘画路径。如果你正在做一个直接的绘画路径(即 BufferStrategy),你可以使用类似的想法与 "loop" 而不是...

public void run() throws InterruptedException {

    int frames = 0;
    Duration threashold = Duration.ofMillis(1000 / 59);
    Duration cycle = Duration.ofSeconds(1);

    Instant cycleStart = Instant.now();

    // main game loop.
    while (isRunning) {
        Instant start = Instant.now();
        // Some update function...

        Thread.sleep(rnd.nextInt(32));

        Duration processTime = Duration.between(start, Instant.now());
        Duration remainingTime = threashold.minusMillis(processTime.toMillis());
        long delay = remainingTime.toMillis();
        if (delay > 0) {
            Thread.sleep(delay);
        } else {
            System.out.println("Dropped frame");
        }

        frames++;
        // Render the output

        Duration cycleTime = Duration.between(cycleStart, Instant.now());
        if (cycleTime.compareTo(cycle) >= 0) {
            cycleStart = Instant.now();
            System.out.println(frames);
            frames = 0;
        }
    }

}

在此示例中,您的更新和绘制调度代码仅需 16 毫秒即可完成工作,否则会丢帧。如果工作花费的时间少于 16 毫秒,则循环将 "wait" 剩余时间以便为 CPU 提供一些喘息空间,以便为其他线程提供时间(并且不会在 CPU)

在上面的示例中,我生成了一个 "random" 长达 32 毫秒的延迟用于测试。将其重新设置为 16,您应该(大约)获得 60fps。

现在,我知道人们对这些东西异常热情,所以如果使用 Thread.sleepDuration 让你起鸡皮疙瘩,你 "could" 使用 "free wheeling" 循环,类似于 Java Main Game Loop

中介绍的内容

下面是一个示例实现,我已将更新数和每秒帧数设置为 60,但您可以更改这些值以满足您的需要...

public void run() throws InterruptedException {

    double ups = 60;
    double fps = 60;

    long initialTime = System.nanoTime();
    final double timeU = 1000000000 / ups;
    final double timeF = 1000000000 / fps;
    double deltaU = 0, deltaF = 0;
    int frames = 0, ticks = 0;
    long timer = System.currentTimeMillis();

    while (isRunning) {

        long currentTime = System.nanoTime();
        deltaU += (currentTime - initialTime) / timeU;
        deltaF += (currentTime - initialTime) / timeF;
        initialTime = currentTime;

        if (deltaU >= 1) {
            Thread.sleep(rnd.nextInt(32));
            //getInput();   
            //update();
            ticks++;
            deltaU--;
        }

        if (deltaF >= 1) {
            Thread.sleep(rnd.nextInt(32));
            //render();
            frames++;
            deltaF--;
        }

        if (System.currentTimeMillis() - timer > 1000) {
            System.out.println(String.format("UPS: %s, FPS: %s", ticks, frames));
            frames = 0;
            ticks = 0;
            timer += 1000;
        }
    }
}

同样,这里的 Thread.sleep 只是为了注入随机数量的 "work"。因为它允许超过 16 毫秒的延迟,所以您还会发现它 "drops" 帧。你的工作是让你的工作时间减少到每次通过 16 毫秒以下