B1向け、Javaゲーム開発の初歩の初歩

この記事は、SLP KBIT Advent Calendar 2015 - Adventar の14日目の記事です。
1年生がオセロの次に行う予定の、Javaを使ったゲーム開発について少し説明しましょう。
Javaで動きのあるゲームを作るにはどうしたらいいのか、少しでもわかってくれるといいなぁ。

JavaC言語の違い

Javaオブジェクト指向言語です。オブジェクト指向というのは、現実の物体(オブジェクト)を単位(クラス)と考えてプログラムを記述するといった感じのものです。
オセロで表すと、C言語ではプレイヤ・盤面・駒の動きをまとめてひとつのファイルに記述していました。しかしJavaでは、プレイヤ・盤面・駒をそれぞれ異なるファイルに記述し、ひとつのオブジェクトとして扱うことができます。このようにすることで、各オブジェクトごとの動きを明確に分けることができ、よりわかりやすいプログラムを記述することができるようになります。
また、オセロで作った盤面や駒といったオブジェクトのファイルは、ほかのゲームでも使うことができます。これはオブジェクト指向の特徴のひとつである、多態性です。

画面を表示してみよう

まずはゲーム画面を表示するためのウインドウを出力してみましょう。Javaファイルには以下のように書き、ファイル名はGameFrame.javaとしてください。

import javax.swing.JFrame;

public class GameFrame extends JFrame {
    public GameFrame() {
        setTitle("Javaゲーム");
    }
    
    public static void main(String args[]) {
        GameFrame gf = new GameFrame();
        gf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        gf.setVisible(true); 
    }
}

それではそのプログラムを実行してみてください。PC画面のおそらく左上に最小のウインドウが開いているはずです。あれば、ドラッグしてウインドウを大きくし、ウインドウタイトルがJavaゲームとなっているか確認してください。

ではプログラムの解説をしていきます。

import javax.swing.JFrame;

これはJFrameというクラスをこのファイルにimportするということを意味しています。

public class GameFrame extends JFrame

class GameFrameでクラス名を宣言しています。つまりこれからGameFrameというクラスを定義するということです。
extends JFrameでは、さきほどimportしたJFrameを、GameFrameクラスに継承させるということを意味しています。

継承というのは、オブジェクト指向の特徴のひとつであり、継承したクラスに定義されているメソッドを使用することができるようになります。
この場合、GameFrameクラスはJFrameクラスに定義されているメソッドを使用することができるということになります。
したがって、以下のメソッドはJFrameクラスで定義されているメソッドということがわかります。

setTitle("Javaゲーム");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true); 

これらのメソッドについても説明します。
setTitle("Javaゲーム")は、その名の通りフレームのタイトルを定義することができます。試しに先ほどのプログラムの("Javaゲーム")を変更してみてください。フレームタイトルが変化することがわかるはずです。

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);は、ウインドウの閉じるボタンを押したときにどのような処理をするかを定義するものです。これは引数によって変化します。この場合はプログラムの終了を意味します。

setVisible(true)は、ウインドウを可視化するときに使います。試しに引数をfalseにしたり、この文をコメントアウトしてみてください、ウインドウが表示されなくなるはずです。

public static void main(String args[]) {
    GameFrame gf = new GameFrame();
    gf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    gf.setVisible(true); 
}

このpublic static void main(String args[])というメソッドが、C言語のmain関数にあたるものになります。
GameFrame gf = new GameFrame();では、GameFrameクラスからgfというインスタンスを作り出しています。
この後の2文はインスタンスgfのメソッドを呼び出しているということになります。

継承の注意

継承は便利ですが、何でもかんでも継承すればいいというわけではありません。継承元と継承先の関係をしっかりと考えておかなければなりません。
具体例を挙げて説明すると、例えば、剣は武器の一種なので、剣クラスは武器クラスを継承することができる。しかし、傷薬は武器ではないので、傷薬クラスは武器クラスを継承することはできない。といった具合です。



円を表示させてみよう

先ほどの状態では、まだ文字通りフレームだけの状態です。花壇に例えると煉瓦で縁取りをしただけです。これから土を敷いて花を植えていきましょう。
それでは花壇の土になるGamePanelクラスを記述していきましょう。以下のコードをGamePanel.javaとして保存してください

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

import javax.swing.JPanel;

public class GamePanel extends JPanel{
    private static final int WIDTH = 240;
    private static final int HEIGHT = 240;

    private static final int SIZE = 10;
    private int x;
    private int y;

    public GamePanel() {
	setPreferredSize(new Dimension(WIDTH,HEIGHT));
	x = 100;
	y = 100;
    }
    
    @Override
    public void paintComponent(Graphics g) {
	super.paintComponent(g);
	g.setColor(Color.RED);
	g.fillOval(x-SIZE/2, y-SIZE/2, SIZE, SIZE);
    }

}

変数を宣言する際、データ型の前に、
private static final
となっていますがこれは、
privateで、このクラス以外での変数の使用ができない。
staticで、この変数が静的な変数である。
finalで、変数の変更ができない。
ということを設定することができます。

setPreferredSize(new Dimension(WIDTH,HEIGHT))
これで、このパネルの推奨サイズを設定します。
これはコンピュータの環境の違いで、正しく表示されないという現象を軽減するためです。
今回は、宣言している変数WIDTH,HEIGHTでサイズを設定したので、このパネルのサイズは240×240になります。

x = 100 y = 100
この変数xとyは円の座標を表します。

ここで注意してほしいのは、コンピュータの画面の座標は数学の座標平面と異なり、左上を原点として右に行くほどxが大きく、下に行くほどyが大きくなるということです。

@Override
オーバーライドというのは継承したクラスのメソッドを上書きすることを言います(たぶん)。
この場合は、JPanelクラスにあるpaintCompornentを上書きしています。
super.paintCompornent()は、書いとかないといけないと考えてください。
g.getColor()で、表示するコンポーネントgの色を設定します。ここでは赤にしています。
色を変えたい場合はREDの箇所を変更してください。
g.fillOval()で、円を描きます。引数は図形の左上のx座標, y座標, 幅, 高さの順です。
x-SIZE/2で描画した円の中心がx, yに来るようにしています。
ほかにも描画したいものがあれば、このメソッドに書き込みます。

これで、(100,100)の位置に円を描いたGamePanelができました。しかしこれではまだ表示することはできません。
フレームに入れてないからです。それでは、表示を行うためにGameFrameクラスを変更しましょう。以下のようにしてください。

import java.awt.Container;

import javax.swing.JFrame;

public class GameFrame extends JFrame {
    public GameFrame() {
        setTitle("Javaゲーム");
        setResizable(false);
        GamePanel gp = new GamePanel();
        Container contentPane = getContentPane();
        contentPane.add(gp);
        pack();
    }

    public static void main(String args[]) {
        GameFrame gf = new GameFrame();
        gf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        gf.setVisible(true);
    }
}

setResizable()
ウインドウのサイズを変更できるか設定することができます。ここではfalseにしているので、大きさは変更できません。

Container contentPane = getContentPane();
これで、コンポーネントを張り付ける場所を準備します。
この処理をしていないと張り付けることができないコンポーネントもあるので注意が必要です。

contentPane.add(gp);
先ほど、準備した場所にGamePanelのインスタンスgpを張り付けます。
これで、GamePanelで設定したとおりに円が描かれた状態で画面が出力されます。

アニメーション風にする

円を描画することまではできましたね。それではその円を動かしてみましょう。
先ほどのコードに移動をメソッドを記述すればできますが、それではオブジェクト指向を使えていないので、新しいクラスを定義します。
円をボールととらえられるので、Ballクラスにしましょう。それでは以下のコードの記述してください。ファイル名はBall.javaとしてください。

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

public class Ball {
    private static final int SIZE = 10;
    private int x,y;
    private int dx, dy;

    public Ball(int x, int y, int dx, int dy) {
        this.x = x;
        this.y = y;
        this.dx = dx;
        this.dy = dy;
    }

    public void move() {
        x += dx;
        y += dy;

        if ( x <= 0 || x > GamePanel.WIDTH-SIZE) {
            dx = -dx;
        }

        if ( y <= 0 || y > GamePanel.HEIGHT-SIZE) {
            dy = -dy;
        }
    }

    public void draw(Graphics g) {
        g.setColor(Color.RED);
        g.fillOval(x, y, SIZE, SIZE);
    }
}

Ballクラスでボールの移動、描画のメソッドを定義しています。
変数x,yはボールの初期位置、dx,dyは移動の速さを表しています。

move()で記述しているif文では、ボールが画面の端にきたら向きを反転させるものです。

円の描画などのをallクラスで行うため、GamePanelクラスを変更します。以下のようにしてください。

import java.awt.Dimension;
import java.awt.Graphics;

import javax.swing.JPanel;

public class GamePanel extends JPanel implements Runnable{
    public  static final int WIDTH = 240;
    public static final int HEIGHT = 240;

    private Ball ball;
    private Thread thread;

    public GamePanel() {
        setPreferredSize(new Dimension(WIDTH,HEIGHT));
        ball = new Ball(100,100,3,6);
        thread = new Thread(this);
        thread.start();
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        ball.draw(g);
    }

    public void run() {
        while (true) {
            ball.move();
            repaint();
            try {
                Thread.sleep(20);
            } catch ( InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

Thread
スレッドとはプログラムの流れのことです。このThreadを記述していなくても、実はThreadはプログラムを実行する際作られます。
それは、mainメソッドをスタートとしたmainThreadです。
これによって同時に複数の処理を行うことができます。この場合は、mainThreadで最初の表示の設定、GemaPanelのrunメソッドに定義されている、ボールの移動と描画を行うThreadの2つになります。
Threadを利用する場合2つの方法があります。1つはThreadクラスをextendsする方法、もう1つはThreadクラスをimplementsする方法です。
今回はJPanelをすでにextendsしているので後者の方法で実装します。

run()
Threadを利用する場合、必ずrunメソッドを記述しなければいけません。Threadがスタートすると行う処理を定義するためです。
この場合は、ボールの移動と描画、20ミリ秒の処理の停止を繰り返し行っています。
処理の停止を行うのは、コンピュータの処理速度が速く、停止をしないとボールの移動が高速になりすぎるからです。
これは大規模なゲームでもあり、FPSを設定するためにも用いられます。(実際はもっと高度な処理をしています)
描画の更新を止めている間に、様々なほかの処理を行っているのです。

応用

これで、Javaで簡単なアニメーションを表示することができましたね。
さらに機能を加えれば、より、ゲームらしくなります。
その前に、プログラムコードの数値をいろいろ変えてみてください。
Threadを停止させる時間や、ボールの位置や移動速度を変えてみると面白いです。
また、ボールを増やせば、少し派手なアニメーションにもなります。
いろいろ遊んでみてください。