UpperHand Gameを作る
必要な部分だけ絵を描く
さて、人が二人でゲームするのに必要な機能はすべてそろいました。
ただ、画面がちらちらして見にくいですね。
今回はこれを改善しましょう。
画面がちらちらするのは、一手打つたびに
repaint()
を使用して画面全体を再描画しているからです。
repaint()
を呼び出してから
paint()
が呼ばれるまでには以下のことが行われています。
repaint()
は、
システムに適当なタイミングで再描画を行うよう依頼する。
- システムは、適切な
Graphics
オブジェクトを取得し、
それを引き数にしてupdate()
を呼び出す。
update()
は、背景色でコンポーネント全体を塗りつぶし、
paint()
を呼び出す。
画面をちらつかせないためには、
getGraphics()
メソッドを使用して
Graphics
オブジェクトを取得し、
それに対して部分的な描画を行えばいいのです。
例えば、UpperHandBoardView
クラスの場合、
玉が置かれた部分だけpaintBall()
を呼び出せば
ちらつきを押さえることができます。
では、だれ(どのオブジェクト)がどのタイミングでpaintBall()
を呼び出せばよいか考えてみましょう。
UpperHand
オブジェクトがmakeMove()
を呼び出した後にpaintBall()
を呼び出したのでは、
ボーナス玉を描画することができません。
UpperHandGame
オブジェクトがmakeMove()
の中で
putBall()
を呼び出すときにpaintBall()
を呼び出すのがよさそうですが、
UpperHandGame
オブジェクトは
UpperHandBoardView
オブジェクトを参照していません。
実は、このような場合に有効なプログラミングテクニックとして、
Smalltalkという言語で古くから使われている
MVC(モデル・ビュー・コントローラ)
という枠組みがあるのです。
MVCとは
モデルは描画対象のデータを表現するクラスです。
UpperHand Gameの場合、UpperHandGame
クラスがモデルに相当します。
ビューはモデルを表示するクラスです。
UpperHand Gameの場合、
UpperHandBoardView
、
UpperHandPlayerView
、
UpperHandBallView
の3つのクラスがビューに相当します。
コントローラはモデルを操作するクラスです。
UpperHand Gameの場合、
UpperHand
クラスがコントローラに相当します。
JavaではMVCを実装するために、Observable
クラスと
Observer
インタフェースが定義されています。
|
Javaには2つのスタイルの継承方法があります。
1つは既存のクラスを拡張(extends)する方法、
もう1つは既存のインタフェースを実装
(implements)する方法です。
インタフェースはクラスに似ていますが、
メソッドを実装するコードが定義されていません。
インタフェースを実装する場合は、
そのインタフェースのすべてのメソッドをオーバーライドする必要があります。
|
モデルになるクラスはObservable
クラスをスーパークラスとします。
ビューになるクラスはObserver
インタフェースを実装します。
ビューはモデルに「依存」します。
Observable
クラスのインスタンスメソッド
addObserver()
の引き数にビューを指定することにより
モデルとビューの依存関係が作られます。
モデル自身にはビューとの依存関係を作るコードは存在しません。
モデルは自分が変化したとき、setChanged()
メソッドを呼び出した後、
「どのように」変化したかを示すオブジェクトを引き数にして
notifyObservers()
メソッドを呼び出します。
するとObservable
クラスの機能によってブロードキャストが行われ、
そのモデルに依存しているすべてのビューの
update()
メソッドが呼び出されます。
それぞれのビューはObserver
インタフェースの
update()
メソッドをオーバーライドし、
どのように変化したかを示すオブジェクトにしたがって
部分的に描画を行えばいいのです。
MVCを実装する
では、MVCを実装しましょう。
まず、どのように変化したかを示すクラスUpperHandEvent
を定義します。
UpperHandEvent
クラスは
変更内容を示すインスタンス変数type
と
変更内容の詳細を示すインスタンス変数
player
、position
を持ちます。
type
|
player
|
position
|
意味
|
INITIALIZE
|
−
|
−
|
盤面が初期化された。
|
PICK_UP_BALL
|
●
|
−
|
player の持ち玉が減った。
|
PUT_BALL
|
−
|
●
|
position に玉を置いた(玉が置けるようになった)。
|
TURN_PLAYER
|
−
|
−
|
手番が完了した。
|
//=============================================================================
// UpperHandEvent
// ゲームの状態が変化したときに通知するイベント。
//=============================================================================
public class UpperHandEvent extends Object
{
//-----------------------------------------------------------------------------
// クラス変数定義
//-----------------------------------------------------------------------------
// 変更内容の定数定義
public final static int INITIALIZE = 0; // 盤面が初期化された
public final static int PICK_UP_BALL = 1; // 持ち玉が減った
public final static int PUT_BALL = 2; // ゲーム盤に玉を置いた
public final static int TURN_PLAYER = 3; // 手番が完了した
//-----------------------------------------------------------------------------
// インスタンス変数定義
//-----------------------------------------------------------------------------
// 変更内容
public int type;
// 玉を減らしたプレーヤー
// type == PICK_UP_BALL のときのみ有効
public int player;
// 玉を置いた位置
// type == PUT_BALL のときのみ有効
public int position;
//-----------------------------------------------------------------------------
// コンストラクタ定義
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// public UpperHandEvent(int type, int player, int position)
// UpperHandEventのインスタンスを生成し、初期化を行う。
//-----------------------------------------------------------------------------
public UpperHandEvent(int type, int player, int position)
{
this.type = type;
this.player = player;
this.position = position;
}
}
|
UpperHandEvent
クラスは
UpperHandGame.java
に記述しました。
Javaでは1つのソースファイルに2つのクラスを定義することもできます。
次に、UpperHandGame
クラスにモデルとしての機能を追加します。
UpperHandGame
クラスを
Observable
クラスのサブクラスとし、
盤面が変化したときビューに変化を通知するコードを追加します。
import java.util.*;
...
//=============================================================================
// UpperHandGame
// UpperHandのゲーム盤を定義するクラス。
//=============================================================================
public class UpperHandGame extends Observable
{
...
}
|
//-----------------------------------------------------------------------------
// public void init()
// ゲーム盤を初期状態にする。
//-----------------------------------------------------------------------------
public void init()
{
// 位置ごとの状態を初期化する。
for (int p = 0; p < boardStatus.length; p++)
{
if (downPosition[p][0] == -1)
{ // 最下段の場合
boardStatus[p] = MOVABLE; // 玉が置ける
}
else
{ // その他
boardStatus[p] = UNMOVABLE; // 玉は置けない
}
}
boardStatus[12] = NUTRAL; // 最下段の中央に中立の玉を置く。
// 持ち玉の数を初期化する。
ball[FIRST] = 27;
ball[SECOND] = 27;
// 次のプレーヤーを初期化する。
nextPlayer = FIRST;
// 初期化が完了したことを通知する。
setChanged();
notifyObservers(new UpperHandEvent(UpperHandEvent.INITIALIZE, -1, -1));
}
|
//-----------------------------------------------------------------------------
// private void putBall(int p, int player)
// pで指定された位置にplayerの玉を置く。
//-----------------------------------------------------------------------------
private void putBall(int p, int player)
{
if (ball[player] == 0)
{ // 持ち玉がない場合
return; // 何もしない。
}
// 持ち玉を減らす。
ball[player]--;
// 持ち玉が減ったことを通知する。
setChanged();
notifyObservers(
new UpperHandEvent(UpperHandEvent.PICK_UP_BALL, player, -1));
// 盤に玉を置く。
boardStatus[p] = player;
// 玉を置いたことを通知する。
setChanged();
notifyObservers(new UpperHandEvent(UpperHandEvent.PUT_BALL, -1, p));
}
|
//-----------------------------------------------------------------------------
// public void makeMove(int p)
// pで指定された手を打つ。
//-----------------------------------------------------------------------------
public void makeMove(int p)
{
if (isFinish())
{ // ゲームが終了していたとき。
throw new Error("Game already finished"); // エラーとする。
}
else
if (boardStatus[p] != MOVABLE)
{ // 着手不能な場所の場合
throw new Error("Bad move"); // エラーとする。
}
// まず、指定された位置に玉を置く。
putBall(p, nextPlayer());
// 次に、ボーナス玉を置く。
checkBonus:
for (int i = 0; i < boardStatus.length; i++)
{
if (boardStatus[i] == UNMOVABLE)
{ // 着手不可能な位置の場合、ボーナス玉の判定を行う。
int first = 0; // 1つ下の先手の玉の数(0で初期化)
int second = 0; // 1つ下の後手の玉の数(0で初期化)
// 1つ下の玉の状態を調べる。
for (int d = 0; d < 4; d++)
{
switch (boardStatus(downPosition[i][d]))
{
case FIRST:
first++;
break;
case SECOND:
second++;
break;
case NUTRAL:
break;
default:
// 1箇所でも下に玉がない場合、ボーナス玉の判定は不要。
// 次の位置のボーナス玉の判定を行う。
continue checkBonus;
}
}
if (first >= 3)
{ // 先手の玉が3つ以上あった場合
putBall(i, FIRST); // 先手のボーナス玉を置く。
}
else
if (second >= 3)
{ // 後手の玉が3つ以上あった場合
putBall(i, SECOND); // 後手のボーナス玉を置く。
}
else
{ // どちらの玉も3つ以上ない場合
boardStatus[i] = MOVABLE; // 玉が置けるようにする。
// 玉が置けるようになったことを通知する。
setChanged();
notifyObservers(
new UpperHandEvent(UpperHandEvent.PUT_BALL, -1, i));
}
}
}
// プレーヤーを交代する。
nextPlayer = (nextPlayer == FIRST) ? SECOND : FIRST;
// 手番が完了したを通知する。
setChanged();
notifyObservers(new UpperHandEvent(UpperHandEvent.TURN_PLAYER, -1, -1));
}
|
最後に、各コンポーネントにビューとしての機能を追加します。
Observer
インタフェースを実装し、
コンストラクタでモデルとの依存関係を作り、
update()
メソッドをオーバーライドします。
UpperHandBoardView
クラスでは、玉が置かれたとき
置かれた玉のみを描画しています。
ゲームが初期化されたときは、「即座」に盤面を再描画するために
repaint()
ではなく
update()
を直接呼び出しています。
import java.util.*;
//=============================================================================
// class UpperHandBoardView
// ゲーム盤を表示するクラス。
//=============================================================================
public class UpperHandBoardView extends Canvas
implements Observer
{
...
}
|
//-----------------------------------------------------------------------------
// public UpperHandBoardView(UpperHandGame game, UpperHand app)
// UpperHandBoardViewのインスタンスを生成し、初期化を行う。
// UpperHandGame game - 表示対象のゲーム
// UpperHand app - ゲームを制御するオブジェクト
//-----------------------------------------------------------------------------
public UpperHandBoardView(UpperHandGame game, UpperHand app)
{
this.game = game;
this.app = app;
// コンポーネントの背景色を設定する。
setBackground(UpperHand.color[UpperHand.BG_COLOR]);
// ゲームとの依存関係を作る。
game.addObserver(this);
}
|
//-----------------------------------------------------------------------------
// public void update(Observable o, Object arg)
// ゲームの状態が変化したときの処理を行う。
//-----------------------------------------------------------------------------
public void update(Observable o, Object arg)
{
UpperHandEvent evt = (UpperHandEvent)arg;
switch (evt.type)
{
case UpperHandEvent.INITIALIZE: // ゲームが初期化されたとき
// ゲーム盤全体を再描画する。
update(getGraphics());
break;
case UpperHandEvent.PUT_BALL: // 玉が置かれたとき
// 置かれた玉のみ描画する。
paintBall(getGraphics(), evt.position);
break;
}
}
|
UpperHandBallView
クラスには、
新規に玉を消すメソッドpickUpBall()
を追加しました。
自分の持ち玉が減ったときにpickUpBall()
を呼び出しています。
ゲームが初期化されたときは、UpperHandBoardView
クラスと同様にupdate()
を直接呼び出すようにしました。
import java.util.*;
//=============================================================================
// class UpperHandBallView
// プレーヤーの持ち玉を表示するクラス。
//=============================================================================
public class UpperHandBallView extends Canvas
implements Observer
{
...
}
|
//-----------------------------------------------------------------------------
// public UpperHandBallView(UpperHandGame game, int player))
// UpperHandBallViewのインスタンスを生成し、初期化を行う。
// UpperHandGame game - 表示対象のゲーム
// int player - 表示対象のプレーヤー
//-----------------------------------------------------------------------------
public UpperHandBallView(UpperHandGame game, int player)
{
this.game = game;
this.player = player;
// 玉の色を決める。
switch (player)
{
case UpperHandGame.FIRST:
this.color = UpperHand.color[UpperHand.FIRST_COLOR];
break;
case UpperHandGame.SECOND:
this.color = UpperHand.color[UpperHand.SECOND_COLOR];
break;
default:
throw new Error("Bad player");
}
// コンポーネントの背景色を設定する。
setBackground(UpperHand.color[UpperHand.BG_COLOR]);
// ゲームとの依存関係を作る。
game.addObserver(this);
}
|
//-----------------------------------------------------------------------------
// private void pickUpBall(Graphics g)
// 減った分の玉を消す。
//-----------------------------------------------------------------------------
private void pickUpBall(Graphics g)
{
// 玉を消す。
g.setColor(UpperHand.color[UpperHand.BG_COLOR]);
int p = 0;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
if (p >= game.ball(player))
{
g.fillRect(size * x + 1, size * y + 1, size - 2, size - 2);
}
p++;
}
}
}
|
//-----------------------------------------------------------------------------
// public void update(Observable o, Object arg)
// ゲームの状態が変化したときの処理を行う。
//-----------------------------------------------------------------------------
public void update(Observable o, Object arg)
{
UpperHandEvent evt = (UpperHandEvent)arg;
switch (evt.type)
{
case UpperHandEvent.INITIALIZE: // ゲームが初期化されたとき
// コンポーネント全体を再描画する。
update(getGraphics());
break;
case UpperHandEvent.PICK_UP_BALL: // 持ち玉が減ったとき
if (evt.player == player)
{ // 自分の持ち玉の場合
// 玉を消す。
pickUpBall(getGraphics());
}
break;
}
}
|
UpperHandPlayerView
クラスでは、
ゲームが初期化されたときと、手番が完了したときに
update()
を直接呼び出しています。
import java.util.*;
//=============================================================================
// class UpperHandPlayerView
// プレーヤーの名前、手番を表示するクラス。
//=============================================================================
public class UpperHandPlayerView extends Canvas
implements Observer
{
...
}
|
//-----------------------------------------------------------------------------
// public UpperHandPlayerView(UpperHandGame game, int player)
// UpperHandBPlayerViewのインスタンスを生成し、初期化を行う。
// UpperHandGame game - 表示対象のゲーム
// int player - 表示対象のプレーヤー
//-----------------------------------------------------------------------------
public UpperHandPlayerView(UpperHandGame game, int player)
{
this.game = game;
this.player = player;
// コンポーネントの背景色を設定する。
setBackground(UpperHand.color[UpperHand.BG_COLOR]);
// ゲームとの依存関係を作る。
game.addObserver(this);
}
|
//-----------------------------------------------------------------------------
// public void update(Observable o, Object arg)
// ゲームの状態が変化したときの処理を行う。
//-----------------------------------------------------------------------------
public void update(Observable o, Object arg)
{
UpperHandEvent evt = (UpperHandEvent)arg;
switch (evt.type)
{
case UpperHandEvent.INITIALIZE: // ゲームが初期化されたとき
case UpperHandEvent.TURN_PLAYER: // 手番が完了したとき
// コンポーネント全体を再描画する。
update(getGraphics());
break;
}
}
|
UpperHand
クラスからは
すべてのコンポーネントを再描画するコードを取り除きました。
//-----------------------------------------------------------------------------
// public void startGame()
// ゲームを開始する。
//-----------------------------------------------------------------------------
public void startGame()
{
// プレーヤーの名前を設定する。
playerView[UpperHandGame.FIRST].playerName("First player");
playerView[UpperHandGame.SECOND].playerName("Second player");
// ゲームを初期化する。
game.init();
}
|
//-----------------------------------------------------------------------------
// public void makeMove(int p)
// 位置pに着手する。
//-----------------------------------------------------------------------------
public void makeMove(int p)
{
// 着手する。
game.makeMove(p);
}
|
ここまでにできあがったもの
Javaソースコード (Ver. 1.1a7)
Satoshi Kobayashi
(koba@yk.rim.or.jp)