チーム開発記

はじめに

この記事はこの記事は、SLP_KBIT Advent Calendar 2016 の20日目の記事です。
最近、チームで簡単なゲーム開発を行ったので、そのことについて書いていきます。

ゲーム内容

今回開発を行ったのは、Pongのような2Dの卓球ゲームをベースに、ブロック崩しの要素を加えた対戦ゲームです。
これだけではゲームとして物足らないので、3種類の必殺技を追加し、対戦ゲームとしての戦略性の向上を目指しました。 言語はJavaで、Eclipseで開発を行いました。ソースコードの共有にはGithubをもちいました。開発期間はおよそ1か月です。

チーム開発

今回のゲームは3人で開発を行いました。一応自分はチームの代表者として、開発スケジュールやメンバへのタスク割当などをしました。(一応コードも書きました)
開発自体は、前半、中盤、後半の3段階に分けて、進めていきました。 タスク管理などは、部屋にあったホワイトボードに今日のやることを書いておき、終われば消すという古典的な方法をしてました(大笑)

前半

前半はゲーム開発に向けて、どんなクラスが必要になるのか、各クラスにどれだけの責任を持たせるのかについて話し合いました。
そして、以下のクラスを設計することになりました。

  • CBYC_Bong
    メインのクラス
  • GameObject
    ボールやブロックなどのオブジェクトの共通情報を扱う
  • Field
  • Wall
  • Puck
  • Racket
  • Block

  • Character
    プレイヤの共通情報を扱う

  • Player
  • GameSound

中盤

中盤では、前半で考えた設計を基に、担当を割り振って各クラスの実装を行っていきました。
クラスの実装ができたら、それぞれの動作を確認し、バグの確認を行っていきました。 実装を行っていくうえで、もっと細分化できるクラスはないか、実装した方がいいクラスはないかを確認していきました。 そこで、新たに以下のクラスを設計実装を行いました。

  • DeadlyGauge
    必殺技ゲージ
  • Goal
    Wallクラスを継承
  • Manager
    オブジェクトの初期位置などの定数を管理
  • ObjectManager
    Block, Wall, Goal, Puck, Field, Racketのインスタンスを管理 オブジェクトの移動などはこのクラスを介して行う
  • PlayerManager
    Playerのインスタンスを管理
  • Operation
    操作説明を表示

さらに、ゲームらしくするために、タイトル画面を設計し、画面遷移が行えるようにしたり、画像やBGMとなる音声などを集め、実装していきました。 そこで、画面設計を担当するクラスを新に実装しました。

  • StartScreen
  • WinScreen

終盤

最後の仕上げとして、必殺技のバランスの調整や、使いやすさの向上を目指して、テストプレイを何十人かに行ってもらい、その感想からキー配置を変えたりしました。
テストプレイを行うことで発見できたバグもありました。
最後の最後まで、チーム全体でもっと面白いゲームに仕上げようと様々な案が出てきました。そのうちのいくつかを紹介します。

  • 2回当てないと壊せないブロック
  • 必殺技発動時にカットイン
  • ボタンを押していることをゲーム画面にも表示

まとめ

最近行ったチーム開発についてざっくりと紹介しました。
いいものを作るには、チーム全体で同じ方向を向いていることが大切なので、メンバとはしっかりと話し合い、実装を行っていきました。
タスク管理などはGithubのissueを使えばいいのに...だらしねぇと言われるかもしれませんが、その時はよく使い方がわからなかったので、仕方ないね。
チームで作業をすると、自分が知らない方法もメンバが知っていたりと、非常にいい勉強になります。
皆さんも、また同じメンバでなにか作りたいと思えるダチを見つけましょう!!

ちなみに開発したゲームのコードはGithubにあります。 GitHub - sekishin/CBYC_Bong

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を停止させる時間や、ボールの位置や移動速度を変えてみると面白いです。
また、ボールを増やせば、少し派手なアニメーションにもなります。
いろいろ遊んでみてください。