[Next] [Prev] [Back] [Home]

■ UpperHand Gameを作る


必要な部分だけ絵を描く

さて、人が二人でゲームするのに必要な機能はすべてそろいました。 ただ、画面がちらちらして見にくいですね。 今回はこれを改善しましょう。

画面がちらちらするのは、一手打つたびに repaint()を使用して画面全体を再描画しているからです。 repaint()を呼び出してから paint()が呼ばれるまでには以下のことが行われています。

  1. repaint()は、 システムに適当なタイミングで再描画を行うよう依頼する。
  2. システムは、適切なGraphicsオブジェクトを取得し、 それを引き数にしてupdate()を呼び出す。
  3. update()は、背景色でコンポーネント全体を塗りつぶし、 paint()を呼び出す。

画面をちらつかせないためには、 getGraphics()メソッドを使用して Graphicsオブジェクトを取得し、 それに対して部分的な描画を行えばいいのです。 例えば、UpperHandBoardViewクラスの場合、 玉が置かれた部分だけpaintBall()を呼び出せば ちらつきを押さえることができます。

では、だれ(どのオブジェクト)がどのタイミングでpaintBall() を呼び出せばよいか考えてみましょう。
UpperHandオブジェクトがmakeMove() を呼び出した後にpaintBall()を呼び出したのでは、 ボーナス玉を描画することができません。 UpperHandGameオブジェクトがmakeMove()の中で putBall()を呼び出すときにpaintBall() を呼び出すのがよさそうですが、 UpperHandGameオブジェクトは UpperHandBoardViewオブジェクトを参照していません。
実は、このような場合に有効なプログラミングテクニックとして、 Smalltalkという言語で古くから使われている MVC(モデル・ビュー・コントローラ) という枠組みがあるのです。

MVCとは

モデルは描画対象のデータを表現するクラスです。 UpperHand Gameの場合、UpperHandGameクラスがモデルに相当します。
ビューはモデルを表示するクラスです。 UpperHand Gameの場合、 UpperHandBoardViewUpperHandPlayerViewUpperHandBallView の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と 変更内容の詳細を示すインスタンス変数 playerpositionを持ちます。

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に対応していません。

Javaソースコード (Ver. 1.1a7)



[Next] [Prev] [Back] [Home]
Satoshi Kobayashi (koba@yk.rim.or.jp)