Java 2d游戏 - 控制动画速度

时间:2018-06-10 22:30:13

标签: java animation model-view-controller game-physics 2d-games

我制作了一个小型小行星游戏,我在控制动画速度方面遇到了一些麻烦,例如,假设我的游戏中有20个小行星,当我摧毁一颗小行星时,小行星的数量会下降(显然),因为游戏中的物体较少,fps上升,小行星的动画速度越来越快,我通过根据游戏中小行星的数量调整动画速度来修复它,但我也面对另一个当我摧毁一颗小行星时爆炸的问题,我可以做我对小行星所做的同样的事情,但我认为它不是一个非常明智的方法来“解决”它,对我来说似乎是不好的做法。我想过限制fps,但我不确定该怎么做,如果它甚至是我应该做的事情,它是我制作的第二款游戏,它是一款非常小的游戏,所以我不太熟悉这个概念和设计我想得到一些建议,以及处理这种情况的最佳方法。

我将在这里发布我的主要游戏类,包括游戏循环,以及爆炸类的一个例子,这样你就可以得到代码的一般概念,我不想把太多代码放在一起,不要混淆,如果需要更多代码,我会在这里更新。

游戏类和循环:

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

爆炸类:

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

一个小问题,在我的游戏中我使用Enum作为对象ID,我试图在我的游戏中实现MVC,我不太确定它,一切都进入其中一个(模型,控制器,视图) ?如果没有,我只是把Enum是一个单独的通用包吗?感谢先进的帮助!

1 个答案:

答案 0 :(得分:1)

您的主循环产生不均匀的更新。如果我什么也不做,我会在77999138284754 fps之间的任何地方,但是,如果我投入8毫秒延迟(模拟一些工作),它会降到{{1}左右} - 115 fps。

您的目的是尽量让帧速率尽可能均匀,这样可以确保动画速度保持不变

就个人而言,我不喜欢游戏循环的“自由转动”风格,这意味着循环被允许消耗CPU周期而不实际做任何事情,这些循环可以用来做更重要的工作,就像更新UI一样。

在大多数情况下,我只使用Swing 120设置为Timer毫秒间隔,然后使用日期/时间API来计算现在和最后一次更新之间的差异并制作关于做什么的选择,但是,这假设你使用基于Swing的绘画路径。如果你正在做一个直接的绘画路径(即5),你可以使用类似的想法而不是“循环”......

BufferStrategy

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

在上面的例子中,我生成了一个高达32毫秒的“随机”延迟用于测试。将它设置回16,你应该(大致)60fps。

现在,我知道人们对这些事情非常热衷,所以如果使用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; } } } Thread.sleep让你的皮肤爬行,你就“可以”使用“自由转动”循环,类似于在Java Main Game Loop

中提供

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

Duration

同样,这里的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; } } } 只是为了注入随机数量的“工作”。因为它允许超过16毫秒的延迟,你也会发现它“丢弃”帧。你的工作就是让你每次通过不到16毫秒