マイペースなプログラミング日記

DTMやプログラミングにお熱なd-kamiがマイペースに書くブログ

シューティング作成を目指して(3) 〜とりあえずクラス分け編〜

なんか間があいてしまったが、あまり作業は進んでない。なぜなら作業が進める→ブログを書くなわけだけど、ブログを書くまでは次の作業に移らないことにしてるのでブロッキングしてるから。ブログを書くために呼び出されているd-kami#thinkが必要以上に時間をかけてるので先にすすまないわけだ。あと最近眠いのでブログ書くためのエネルギーが足りない。まぁ、マイペースなんだから仕方がない。

それで今回やったことは、とりあえずクラス分け。といってもファイルは5つだし、列挙型や無名内部クラスはあるけど、たいした量じゃない。そして一応、今の段階で頑張って考えてはいるが、なにせ経験のないものだからどうなるかはわからんという状況なので、あとでいろいろ変わるだろうなと思う。とりあえず今回作ったパッケージと、それに属するソースを書いておく


デフォルトパッケージ
Main.java

game
Game.java

game.object
GameObject.java
DrawData.java

game.object.character
Glenda.java

この中で最初に考えたのはGameObject。これはゲーム中にでてくるオブジェクト(物体の意味でオブジェクト指向のものとは別)を表すクラス。プレイヤーキャラや敵などの座標や画像などは共通するものなのでここで定義している。それでプレイヤーキャラと敵専用のクラスを派生させる。各キャラクターの行動は一定時間毎に

for(GameObject character : characterList)
    character.action();

のように扱いたいので、ここでabstractなactionメソッドを定義しておく。本当ならプレイヤーキャラ、敵、弾、画面のサイズなどを持ったクラスを用意して、actionの引数にするべきだろうがまだ作ってない。全てを用意しなくていいから、形だけは作っておけよって話だが。あとaddImageというメソッドで画像を追加するようにしているが、最初に画像が揃ってるのが普通なんだから、まとめて渡すべきだろうなぁ、と今思った。

package game.object;

import java.util.List;
import java.util.ArrayList;

import java.awt.Point;
import java.awt.Image;

/**
  * ゲーム中にでてくるオブジェクトを表すクラス。
  * 名前、画像、座標を持っている
  *
  * @author d-kami
  * @version 1.0
  */
public abstract class GameObject{
    /** GameObjectの属するグループ */
    public enum Group{
        /** プレイヤーが操作するオブジェクトのグループ */
        Player,

        /** 敵を表すオブジェクトのグループ */
        Enemy,

        /**  プレイヤーの弾を表すオブジェクトのグループ */
        PlayerBullet,

        /** 敵の弾を表すオブジェクトのグループ */
        EnemyBullet,

        /** その他のオブジェクトのグループ */
        Other
    }

    /** このオブジェクトの名前 */
    protected final String name;

    /** このオブジェクトが属しているグループ */
    protected final Group group;

    /** このオブジェクトで使う画像のリスト */
    protected final List<Image> imageList;

    /** このオブジェクトを表示するX座標 */
    protected int currentX;

    /** このオブジェクトを表示するY座標 */
    protected int currentY;

    /**
      * 名前とグループを設定する
      *
      * @param name このオブジェクトの名前
      * @param group このオブジェクトが属するグループ
      */
    public GameObject(String name, Group group){
        this.name = name;
        this.group = group;

        imageList = new ArrayList<Image>();
    }

    /**
      * 名前、グループ、画像を設定する
      *
      * @param name このオブジェクトの名前
      * @param group このオブジェクトが属するグループ
      * @param image このオブジェクトで使う画像
      */
    public GameObject(String name, Group group, Image image){
        this(name, group);

        addImage(image);
    }

    /**
      * このオブジェクトで使う画像を追加する
      *
      * @param image このオブジェクトで使う画像
      */
    public void addImage(Image image){
        imageList.add(image);
    }

    /**
      * このオブジェクトが移動する座標を設定する
      *
      * @param x このオブジェクトのX座標
      * @param y このオブジェクトのY座標
      * @return 設定した座標をPoint型にして返す
      */
    protected Point move(int x, int y){
        currentX = x;
        currentY = y;

        return new Point(x, y);
    }

    /**
      * このオブジェクトの行動を定義するメソッド。行動後の座標と表示に使う画像を返す
      *
      * @return このオブジェクトの移動先と表示する画像を格納したクラス
      */
    public abstract DrawData action();
}

次は上のクラスでactionの返り値になっているDrawData。キャラクターの行動した結果、どこに移動したか、とどんな画像で表示するかをまとめたクラス。ただ座標と画像持ってるだけ。どうせ一度きりの設定で、あとは初期化しかしないんだからインスタンス変数は全てfinalにしてコンストラクタの初期化で十分あとは思うんだが、もしかしたら…というよくわからない不安により何度でも設定可能に。多分意味ないと思う。Pointとintで設定できるようにしているが、これも意味ないと思われる

package game.object;

import java.awt.Point;
import java.awt.Image;

/**
  * 描画に使うための情報を格納するクラス
  *
  * @author d-kami
  * @version 1.0
  */
public class DrawData{
    /** 描画するX座標 */
    private int x;

    /** 描画するY座標 */
    private int y;

    /** 描画する画像 */
    private Image image;

    /**
      * 描画する座標を設定する
      *
      * @param point 座標
      */
    public void setPoint(Point point){
        x = point.y;
        y = point.y;
    }

    /**
      * 描画するX座標を設定する
      *
      * @param x 描画するX座標
      */
    public void setX(int x){
        this.x = x;
    }

    /**
      * 描画するX座標を返す
      *
      * @return 描画するX座標
      */
    public  int getX(){
        return x;
    }

    /**
      * 描画するY座標を設定する
      *
      * @param y 描画するY座標
      */
    public void setY(int y){
        this.y = y;
    }

    /**
      * 描画するY座標を返す
      *
      * @return 描画するY座標
      */
    public int getY(){
        return y;
    }

    /**
      * 描画する画像を設定する
      *
      * @param image 描画する画像
      */
    public void setImage(Image image){
        this.image = image;
    }

    /**
      * 描画する画像を返す
      *
      * @return 描画する画像
      */
    public Image getImage(){
        return image;
    }
}

次にプレイヤーキャラであるGlenda。キーボードイベントが発生した時に座標を変えてるが、どのキーが押されたかを保存しておきactionで座標を変えた方がいいのではないかという思いが時間とともに大きくなっている。そもそも、ここでキーボードイベントを処理するのが…。何か作っておいてマイナスな点ばかりが並ぶ。

package game.object.character;

import java.awt.Image;
import java.awt.event.KeyListener;
import java.awt.event.KeyEvent;

import game.object.GameObject;
import game.object.DrawData;

/**
  * プレイヤーが操作するキャラクターであるGlenda(Plan9のマスコット)を表すクラス
  */
public class Glenda extends GameObject implements KeyListener{
    /** キーボードを押したときに移動する距離 */
    private static final int MOVE_DISTANCE = 4;

    /** キーボードで移動できるかどうか。キーボードを押してからactionが呼ばれるまでキーボードでの移動はしない */
    private boolean canMove;

    /**
      * 画像を受け取り、初期化する
      *
      * @param image Glendaの画像
      */
    public Glenda(Image image){
        super("Glenda", GameObject.Group.Player, image);

        canMove = true;
    }

    /**
      * Glendaを移動させる。移動先を決めた後、キーボードでの移動を許可する
      */
    @Override
    public DrawData action(){
        DrawData data = new DrawData();
        data.setX(currentX);
        data.setY(currentY);
        data.setImage(imageList.get(0));

        canMove = true;

        return data;
    }

    /**
      * キーボードでGlendaの移動先を決める。一度呼ばれると、actionが呼ばれるまで
      * 何度呼ばれても何もしない。
      *
      * @param e キーボードイベントで使うデータ
      */
    public void keyPressed(KeyEvent e){

        if(!canMove)
            return;

        switch(e.getKeyCode()){
            case KeyEvent.VK_UP:
                move(currentX, currentY - Glenda.MOVE_DISTANCE);
                break;

            case KeyEvent.VK_DOWN:
                move(currentX, currentY + Glenda.MOVE_DISTANCE);
                break;

            case KeyEvent.VK_RIGHT:
                move(currentX + Glenda.MOVE_DISTANCE, currentY);
                break;

            case KeyEvent.VK_LEFT:
                move(currentX - Glenda.MOVE_DISTANCE, currentY);
                break;

            default:
                break;
        }

        canMove = false;
    }

    /** 何もしない */
    public void keyReleased(KeyEvent e){}
    /** 何もしない */
    public void keyTyped(KeyEvent e){}
}

次にゲームの重要となる部分。Timerで一定間隔毎にrepaint呼び出して、全てのGameObjectの更新、描画を行っている。初期化のinitを用意したのはコンストラクタにいろいろ処理を任せてしまうのは気がひけたから。

package game;

import java.awt.Color;
import java.awt.Image;
import java.awt.Graphics;
import java.awt.Dimension;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;

import java.io.File;
import java.io.IOException;

import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;

import game.object.DrawData;
import game.object.GameObject;
import game.object.character.Glenda;

/**
  * Glenda(Plan9のマスコット)が活躍するゲームをするためのコンポーネント
  *
  * @author d-kami
  * @version version
  */
public class Game extends JComponent implements ActionListener{
    /** プレイヤーが操作するキャラクター */
    private Glenda glenda;

    /** ゲームに登場するオブジェクトのリスト */
    private List<GameObject> objectList;

    /** 画像を名前で管理するためのMap */
    private Map<String, Image> imageMap;

    /** 描画する間隔を設定するタイマー */
    private Timer timer;

    /**
      * 一部の変数を初期化しているが、画像の読み込みやゲームの設定はinitで行っている
      */
    public Game(){
        objectList = new ArrayList<GameObject>();
        imageMap = new HashMap<String, Image>();
    }

    /**
      * ゲームをするための初期化を行う。ゲームで使う画像を読み込んだり、
      * フォーカス、キーボードイベントの移譲先、タイマーの設定を行っている
      *
      * @throws java.io.IOException 画像の読み込みに失敗した場合に発生
      */
    public void init() throws IOException{
        loadImage("glenda", "./glenda_32.png");

        glenda = new Glenda(imageMap.get("glenda"));
        addGameObject(glenda);

        setFocusable(true);
        addKeyListener(glenda);

        timer = new Timer(50, this);
        timer.start();
    }

    /**
      * ゲームに登場するオブジェクトを追加する
      *
      * @param object ゲームに登場するオブジェクト
      */
    private void addGameObject(GameObject object){
        objectList.add(object);
    }

    /**
      * 画像を読み込み、nameで取得できるようにする
      *
      * @param name 画像の取得に使う文字列
      * @param fileName 読み込む画像のファイルの名前
      * @throws java.io.IOException 画像の読み込みに失敗したら発生する
      */
    private void loadImage(String name, String fileName) throws IOException{
        imageMap.put(name, Game.loadImage(fileName));
    }

    /**
      * 画像を読み込んで返す
      *
      * @param fileName 読み込む画像のファイルの名前
      * @throws java.io.IOException 画像の読み込みに失敗したら発生する
      */
    private static Image loadImage(String fileName) throws IOException{
        return ImageIO.read(new File(fileName));
    }

    /**
      * タイマーで定期的に呼び出される。repaintで再描画要求をだす。
      *
      * @param e ActionEvent、残念ながら使ってない
      */
    public void actionPerformed(ActionEvent e){
        repaint();
    }

    /**
      * 背景を塗りつぶしたりオブジェクトの描画を行う。
      *
      * @param g 描画先
      */
    @Override
    protected void paintComponent(Graphics g){
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, getWidth(), getHeight());

        for(GameObject object : objectList){
            paintGameObject(g, object);
        }
    }

    /**
      * オブジェクトの行動させ、その結果を描画する
      *
      * @param g 描画先
      * @param object 描画するオブジェクト
      */
    private void paintGameObject(Graphics g, GameObject object){
        DrawData data = object.action();

        int x = data.getX();
        int y = data.getY();
        Image image = data.getImage();

        g.drawImage(image, x, y, this);
    }
}

最後にエントリポイント。Gameクラスのインスタンス作り初期化して、ウインドウに追加してウインドウ表示を行っている

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

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

import java.io.IOException;

import game.Game;

/**
  * プログラムのエントリポイントを持つクラス。ウインドウの表示や初期化をやっている
  *
  * @author d-kami
  * @version 1.0
  */
public class Main{
    /**
      * エントリポイント。EventDispatchThreadでMain.startを呼び出すように指令する
      *
      * @param args 使われてない可愛そうな引数
      */
    public static void main(String[] args){
        SwingUtilities.invokeLater(new Runnable(){
            public void run(){
                Main.start();
            }
        });
    }

    /**
      * ウインドウの表示や初期化を行うメソッド
      */
    private static void start(){
        try{
            JFrame frame = new JFrame("Glenda");

            Game game = new Game();
            game.init();

            game.setPreferredSize(new Dimension(600, 500));
            frame.add(game);

            frame.pack();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        }catch(IOException e){
            String message = "Glendaを読み込めませんでした";
            String title = "エラー";
            JLabel label = new JLabel(message);
            label.setForeground(Color.RED);

            JOptionPane.showMessageDialog(null, label, title, JOptionPane.ERROR_MESSAGE);
        }
    }
}

ここまで書いて、後悔が多い。次は敵と弾を追加するが修正もしておかないと