【Java】ゲームループとFPS指定機能の実装

2018年3月11日

本稿はJavaでゲームループの実装をやってみたいと思います。

マルチスレッドを利用するので、わからない方はこちらの記事でスレッドの増やし方までは知っておいてください。

ゲームループの実装

ゲームループを作成するには定期的にpaintComponentメソッドが呼び出されるようにしなければなりません。そのためにThreadクラスを利用し、定期的にrepaintメソッドを実行します。

repaintメソッドを呼び出すと、再描画イベントの実行要求を出すことができます。

それではゲームループを実装したGameWindowのアニメーションサンプルです。

import java.awt.Color;
import java.awt.Graphics;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Test{
	public static void main(String[] args) {
		GameWindow gw = new GameWindow("テストウィンドウ",400,300);
		gw.add(new DrawCanvas());
		gw.setVisible(true);
		gw.startGameLoop();
	}
}
class GameWindow extends JFrame implements Runnable{
	private Thread th = null;
	public GameWindow(String title, int width, int height) {
		super(title);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setSize(width,height);
		setLocationRelativeTo(null);
		setResizable(false);
	}

	//ゲームループの開始メソッド
	public synchronized void startGameLoop(){
		if ( th == null ) {
			th = new Thread(this);
			th.start();
		}
	}
	//ゲームループの終了メソッド
	public synchronized void stopGameLoop(){
		if ( th != null ) {
			th = null;
		}
	}
	public void run(){
		//ゲームループ(定期的に再描画を実行)
		while(th != null){
			try{
				Thread.sleep(25);
				repaint();
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
	}
}

class DrawCanvas extends JPanel{
	int x = 0;
	public void paintComponent(Graphics g) {
		super.paintComponent(g);

		g.setColor(Color.BLACK);

		g.drawString("ゲームループアニメーション", x++, 50);
	}
}
実行結果

runメソッドでスレッドが存在している間はゲームループが実行されます。

そのままループを回すとかなりの速度で処理されてしまいますので、Thread.sleep(スレッド停止間隔のミリ秒)を利用し、スレッドの実行を止めています。

今回は25ミリ秒なので40fpsのゲームループということになります。

これでpaintCompnentが定期的に呼び出されるので、中に処理や描画を実装したらOKです。

計算処理と描画処理でスレッドを分ける構成にしても良かったのですが、面倒な実装が増えるうえに内容によっては同期をとらなければならなくなるので、ゲーム処理と描画は全てシングルスレッドのみで実装する構成としました。

描画間隔の指定(FPS設定)

どのくらいの間隔で再描画を呼び出すかですが、通常ゲームでは60fps(1秒間に60回描画)が多いと思います。

しかし私は処理に負荷のかかるゲームはfpsを下げたり上げたりしたいので任意で決められるようなsetFpsメソッドを作成しておくことにしました。

sleepメソッドはミリ秒指定なので、秒をミリ秒に直す必要があります。1秒をミリ秒に直すと1000ミリ秒となります。60fpsの時の1描画あたりのミリ秒を出したい場合は1000/60を計算すれば算出できます。40fpsなら1000/40になります。

この結果をThread.sleepメソッドに指定すれば基本は大丈夫なのですが、60fpsにしたい場合は16.66666・・・ミリ秒のスレッド停止が必要なのです。

このスレッド停止はマイクロ秒単位で指定する方法も一応用意されているのですが、環境依存なのか、ちゃんと処理されないので正確に60fpsにできません。

こうなっては、実装は無理なので、60fpsになるように16ミリと17ミリの停止を適度に調整する処理を入れてしまいましょう。

まず、1000/fpsを計算し、その値を現在時刻に加算し、次の再描画時間を求めます。そのあとsleep前に次の時間と現在時間の差分を算出してsleepメソッドに指定しましょう。

これで、60fpsのパターンにも対応できます。

現在時刻の取得は、System.currentTimeMillisメソッドで取得できます。

わかりやすくするためにFrameRateクラスを作成し、fpsをリアルタイムで表示できるようにしました。

それでは実装したサンプルをご覧ください。

import java.awt.Color;
import java.awt.Graphics;

import javax.swing.JFrame;
import javax.swing.JPanel;

class Test{
	public static void main(String[] args) {
		GameWindow gw = new GameWindow("テストウィンドウ",400,300);
		gw.setFps(60);//60fpsを指定します。
		gw.add(new DrawCanvas());
		gw.setVisible(true);
		gw.startGameLoop();
	}
}
class GameWindow extends JFrame implements Runnable{
	private Thread th = null;
	private double sleepAddTime;
	private int fps=60;
	public GameWindow(String title, int width, int height) {
		super(title);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setSize(width,height);
		setLocationRelativeTo(null);
		setResizable(false);
		setFps(fps);
	}
	public synchronized void startGameLoop(){
		if ( th == null ) {
			th = new Thread(this);
			th.start();
		}
	}
	public synchronized void stopGameLoop(){
		if ( th != null ) {
			th = null;
		}
	}
	public void run(){
		double nextTime = System.currentTimeMillis() + sleepAddTime;
		//60fpsとなるように再描画を呼び出す
		while(th != null){
			try{
				long res = (long)nextTime - System.currentTimeMillis();
				if ( res < 0 ) res = 0;
				Thread.sleep(res);
				repaint();
				nextTime += sleepAddTime;
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
	}
	public void setFps(int fps){
		if ( fps < 10 || fps > 60 ) {
			throw new IllegalArgumentException("fpsの設定は10~60の間で指定してください。");
		}
		this.fps = fps;
		sleepAddTime = 1000.0 / fps;
	}
}
//リアルタイムfps測定用クラス
class FrameRate{
    private long before;
    private int count;
    private float frameRate;
    private final int updateTimeMillis;
    public FrameRate(int updateTimeMillis){
        this.updateTimeMillis = updateTimeMillis;
        before = System.currentTimeMillis();
    }

    public boolean process(){
        long now = System.currentTimeMillis();
        count++;
        if(now - before >= updateTimeMillis){
            frameRate = (float)(count * 1000) / (float)(now - before);
            before = now;
            count = 0;
            return true;
        }
        return false;
    }

    public float getFrameRate(){
        return frameRate;
    }
}
class DrawCanvas extends JPanel{
	int x = 0;
	FrameRate fr = new FrameRate(500);
	public void paintComponent(Graphics g) {
		super.paintComponent(g);

		//実際に何fpsで動いているかリアルタイムで測定
		fr.process();
		g.drawString(fr.getFrameRate()+"FPS", 60, 100);

		g.setColor(Color.BLACK);
		g.drawString("ゲームループアニメーション", x++, 50);
	}
}
実行結果

ソースが長くなってしまいましたが、60fpsにするためのスレッド停止時間計算処理はここでやってます。

	public void run(){
		double nextTime = System.currentTimeMillis() + sleepAddTime;
		//60fpsとなるように再描画を呼び出す
		while(th != null){
			try{
				long res = (long)nextTime - System.currentTimeMillis();
				if ( res < 0 ) res = 0;
				Thread.sleep(res);
				repaint();
				nextTime += sleepAddTime;
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
	}

Java

Posted by nompor