Javaでゲームを作る入門 @ うぃき

鼻からイクラを食べるゲームのソース

最終更新:

takejava

- view
管理者のみ編集可
鼻からイクラを食べるゲームのソースをみて、簡単なゲームの流れを把握してみようというページです。
こういうのもあるんだーぐらいに眺めてみるといいと思います。

フレームを作成するところから、ゲームループまでの流れと、シーン遷移、シーン実装あたりが肝になります。

MainGameクラス
createAndShowGUIメソッドではフレームの準備をしています。BaseWindowListenerリスナークラスですが、Escキーを押されたときに終了するように実装しているだけで、いらないかもです。ちなみにJFrame使えばすっきりします。

doLoadメソッドはなんとなく作っておきました。データプールの準備とかすればよいかと思いますが、何も描画されないただの窓だけでてきて、大容量のデータ読み込みとかされたらユーザからしたらたまったもんじゃないので、ほどほどにしたほうがよいですし、その辺はシーンの初期化処理でやったほうが無難な気もします。

doLoopメソッドはいわゆるゲームループに入るためのメソッドで、中では、シーンの遷移と初期化、描画を永遠とさばいています。

MainGame.changeScene(new Loading());
とすると、次のシーンに移ることができます。
シーンの中で次のシーンに移りたい場合などに備えて、staticメソッドにしてあります。もっと上手い実装がある気がしてなりません。

MainGameクラスのソースコード
package main.framework;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferStrategy;
import java.text.DecimalFormat;

import main.scene.Loading;
import Utilities.FPSKeeper;

public class MainGame {
        // メインメソッド
        public static void main(String[] args) {
                MainGame mainGame = new MainGame();
                System.out.println("mainGame.createAndShowGUI()");
                mainGame.createAndShowGUI();
                System.out.println("mainGame.doLoad()");
                mainGame.doLoad();
                System.out.println("mainGame.doLoop()");
                mainGame.doLoop();
                System.exit(0);
        }

        public FPSKeeper fpsKeeper;
        private Frame frame;
        private Canvas canvas;
        private BufferStrategy bufferStrategy;
        private Scene currScene;
        static private Scene nextScene;

        private void createAndShowGUI() {
                GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
                GraphicsDevice graphicsDevice = graphicsEnvironment.getDefaultScreenDevice();
                GraphicsConfiguration gc = graphicsDevice.getDefaultConfiguration();
                frame = new Frame(gc);
                frame.setTitle("鼻からイクラを食べるゲーム");
                frame.setIgnoreRepaint(true);
                frame.setUndecorated(false);
                frame.setResizable(false);
                canvas = new ExtendsCanvas();
                canvas.setPreferredSize(new Dimension(400, 400));
                canvas.setBackground(Color.ORANGE);
                frame.add(canvas);
                frame.pack();
                frame.addWindowListener(new BaseWindowListener());
                frame.addKeyListener(new BaseWindowListener());
                frame.addComponentListener(new BaseWindowListener());
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
        }

        private void doLoad() {
                fpsKeeper = new FPSKeeper(60);
                changeScene(new Loading());
        }

        private void doLoop() {
                while (true) {
                        // シーン遷移 or シーンリセット
                        if (nextScene != null) {
                                // 前シーンのリスナー削除
                                canvas.removeKeyListener(currScene);
                                canvas.removeMouseListener(currScene);
                                canvas.removeMouseMotionListener(currScene);
                                canvas.removeMouseWheelListener(currScene);
                                // 次のシーンをセット
                                currScene = nextScene;
                                // フレームのタイトルを設定
                                // frame.setTitle(currScene.toString());
                                // シーンの初期化
                                currScene.init(canvas);
                                // リスナー登録
                                // 同じリスナーがすでに登録されていても追加してしまうので、上で一回削除
                                canvas.addKeyListener(currScene);
                                canvas.addMouseListener(currScene);
                                canvas.addMouseMotionListener(currScene);
                                canvas.addMouseWheelListener(currScene);
                                // 次のシーンを初期化
                                nextScene = null;
                                // デバッグ
                                System.out.println(currScene);
                                System.out.println(getMemoryInfo());
                        }

                        Graphics2D g = (Graphics2D) bufferStrategy.getDrawGraphics();
                        if (!bufferStrategy.contentsLost()) {
                                // 詳細描画
                                if (currScene != null)
                                        currScene.update(g);
                                else {
                                        // エラーメッセージ
                                        g.setColor(Color.BLACK);
                                        g.drawString("シーン初期化エラー", 0, 10);
                                }
                                bufferStrategy.show();
                                g.dispose();
                        }
                        fpsKeeper.sleep();
                }
        }

        /**
         * シーンの初期化、描画のトリガー
         *
         * @param scene
         */
        static public void changeScene(Scene scene) {
                nextScene = scene;
        }

        class ExtendsCanvas extends Canvas {
                /**
                 *
                 */
                private static final long serialVersionUID = 2459217713107216917L;

                @Override
                public void addNotify() {
                        super.addNotify();
                        try {
                                canvas.createBufferStrategy(2);
                                bufferStrategy = canvas.getBufferStrategy();
                        } catch (IllegalStateException e) {
                                e.printStackTrace();
                        }
                }
        }

        /**
         * Java 仮想マシンのメモリ総容量、使用量、
         * 使用を試みる最大メモリ容量の情報を返します。
         * @return Java 仮想マシンのメモリ情報
         */
        public static String getMemoryInfo() {
            DecimalFormat f1 = new DecimalFormat("#,###KB");
            DecimalFormat f2 = new DecimalFormat("##.#");
            long free = Runtime.getRuntime().freeMemory() / 1024;
            long total = Runtime.getRuntime().totalMemory() / 1024;
            long max = Runtime.getRuntime().maxMemory() / 1024;
            long used = total - free;
            double ratio = (used * 100 / (double)total);
            String info =
            "Java メモリ情報 : 合計=" + f1.format(total) + "、" +
            "使用量=" + f1.format(used) + " (" + f2.format(ratio) + "%)、" +
            "使用可能最大="+f1.format(max);
            return info;
        }
}

Sceneクラス
シーンとは、フレームいっぱいにひろがる画面1つを指します。
別に一つのシーンだけにしてifやswitchで分岐させてもよいのですが、きっとたいへんなことになるかと思います。
かといって、暗転するだけのシーンだけとか作っても、クラスが増えて余計めんどいことに、なんてことにもなりかねませんので、分け方は実装者次第になるかと思います。
鼻イクラの場合は、以下のように分けて実装しました。
  • ローディング画面
  • タイトル画面
  • ゲーム画面
  • ランキング表示画面?(いまだに未実装T△T)

シーンクラスは抽象クラスです。なので、継承したクラスは必ず、いかのabstractメソッドを実装しなければいけません。
  • abstract protected void initScene();
  • abstract protected void updateScene(Graphics2D g);
直接ではないですが、メインループで呼ばれるメソッドになります。
これらをもれなく実装すれば、シーンはとりあえず完成です。
もし、キー入力やマウス操作を受け付けたい場合は、中身のないリスナーのメソッドを実装しているので、オーバーライドして中身を実装すればいいです。

今回はCanvasクラスを使って描画をしています。各シーンが一つのCanvasクラスを参照するわけですので、別にCanvasクラスをstaticにして使えばよかったのですが、なるべくstaticを使わない実装を目指していたのでこんなことになっています。
例えば、staticにしてしまえば、わざわざinitで受け渡す必要がないので、直接abstractメソッドをたたけるようになるかと思います。

ところで、Canvasをうけとって何をしたかというとサイズの取得しかしていませんでした。
フレームのサイズ変更を可能しても耐えられるようにするために、常にCanvasのサイズ分の描画をするということをしていました。
一般的に、ゲームはサイズ変更不可に設定すると思いますので、サイズをfinal staticで参照するようにしても良いと思います。

Sceneクラスのソースコード
package main.framework;

import java.awt.Canvas;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;

public abstract class Scene implements KeyListener, MouseListener, MouseMotionListener, MouseWheelListener {
        protected Canvas canvas;

        /**
         * シーン初期化
         * シーン遷移、リセットに耐えられるように実装すること
         */
        abstract protected void initScene();

        /**
         * シーン描画
         */
        abstract protected void updateScene(Graphics2D g);

        public void init(Canvas canvas) {
                this.canvas = canvas;
                // シーンの初期化処理
                initScene();
        }

        public void update(Graphics2D g) {
                // シーンのアップデートと描画
                updateScene(g);
        }

        /*
         * 以下、入力に対応するメソッド群
         */

        @Override
        public void mouseClicked(MouseEvent e) {
        }

        @Override
        public void mousePressed(MouseEvent e) {
                // if (codedKeys[16]) // Shiftキーを押しながらのマウスクリックとか
                // queueShiftMouseDown(e.getPoint(), e.getButton());
                // else
                // queueMouseDown(e.getPoint(), e.getButton());
        }

        @Override
        public void mouseReleased(MouseEvent e) {
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {
        }

        @Override
        public void mouseDragged(MouseEvent e) {
        }

        @Override
        public void mouseMoved(MouseEvent e) {
        }

        @Override
        public void mouseWheelMoved(MouseWheelEvent e) {
        }

        @Override
        public void keyTyped(KeyEvent e) {
        }

        public static volatile boolean keys[] = new boolean[256];
        public static volatile boolean codedKeys[] = new boolean[512];

        @Override
        public void keyPressed(KeyEvent e) {
                char key = e.getKeyChar();
                int code = e.getKeyCode();
                if (key != '\uFFFF')
                        keys[key] = true;
                codedKeys[code] = true;
        }

        @Override
        public void keyReleased(KeyEvent e) {
                char key = e.getKeyChar();
                int code = e.getKeyCode();
                if (key != '\uFFFF')
                        keys[key] = false;
                codedKeys[code] = false;
        }
}

Titleクラス
シーンクラスを実装したタイトル画面を表示するクラスです。
initSceneメソッドでは初期化や、データのロード、updateSceneではデータ更新と描画更新をやっています。
マウスクリックで次の画面のゲーム画面に遷移するようにしています。
中で若干きもいことをやっていますが気にしないでください。

Titleクラスのソースコード
html2 plugin Error : このプラグインで利用できない命令または文字列が入っています。
まとめ?
この実装の仕組みの弱点は、シーン遷移すると前のシーンが捨てられることです。例えば、RPGをつくるときに、町とフィールドをそれぞれ一つのシーンとした場合、町に入ってその後、町から出たときにフィールドがない!ってことになりかねません。対応としてシーンを1つのキューと考え、スタック、ポップで対応できるように実装するのがよいかもしれません。
あと、データの受け渡しもできないのが痛いかもしれません。

以上です。参考程度にお願いしますね。これが全てではないはずですので。
上へ

名前:
コメント:

すべてのコメントを見る
記事メニュー
ウィキ募集バナー