人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

アクティブレンダリング

Javaでゲームを作るときの基礎技術としてアクティブレンダリングを取り上げます。repaint()を使わずに自前で描画処理(レンダリング)を行う定石です。repaint()はいつ描画するか自分で決められませんがアクティブレンダリングを使うと自分で決められます。

active_rendering.jar

repaint()による再描画

このサイトのほとんどのゲームは下のような構造になってました。

    /**
     * ゲームループ
     */
    public void run() {
        while (true) {
            // ゲーム状態を更新(ex: ボールの移動)

            // 再描画
            repaint();

            // 休止
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    /**
     * 描画処理
     * @param g 描画オブジェクト
     */
    public void paintComponent(Graphics g) {
        super.paintComponent(g);

        // 背景をクリア

        // オブジェクトを描画(ex.ボールを描画)
    }

ゲームループでゲーム状態を更新してからrepaint()を呼び出してます。repaint()を呼び出すとpaintComponent()が実行され画面が再描画されるという仕組みです。

ただrepaint()を呼び出してもすぐpaintComponent()が実行されるとは限らないので注意が必要です。repaint()はあくまで再描画の要求であって実際に再描画するかはJVM(Java仮想マシン)が決めます。

この制限はスピードが要求されるゲームではきついらしくrepaint()を使うのはよくないようです。私が今まで作ってきた程度のゲームでは問題なさそうですが・・・。

repaint()による再描画の代わりに使われるのがアクティブレンダリング(Active Rendering)という手法です。これは再描画を強制します。

アクティブレンダリング

アクティブレンダリングを使ったときのゲームループです。

    /**
     * ゲームループ
     */
    public void run() {
        while (true) {
            // ゲーム状態を更新(ex: ボールの移動)
            gameUpdate();
            // バッファにレンダリング(ダブルバッファリング)
            gameRender();
            // バッファを画面に描画(repaint()を自分でする!)
            paintScreen();

            // Active Renderingではrepaint()を使わない!

            // 休止
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

gameUpdate()は、ゲーム状態の更新をメソッドにまとめただけです。キモはgameRender()とpaintScreen()です。repaint()とpaintComponent()がない点に注意してください。この2つの代わりがpaintScreen()です。ちなみに全部自作メソッドなのでメソッド名は自由です。

アクティブレンダリングの手順を簡単にまとめます。

  1. gameUpdate()でゲーム状態(ボールの位置、勇者の位置など)を更新する。
  2. gameRender()でバッファと呼ばれる領域にオブジェクト(ボールとか勇者とか)を描画する。バッファに描画しても画面には表示されない!
  3. paintScreen()でバッファの内容を画面に描画する。ここで初めて画面に表示される!

f:id:aidiary:20090828220303g:plain

まずgameRender()から見てみます。ここではダブルバッファリングという技術を使います。昔のアプレットではおなじみなんですが、近年のSwingコンポーネントでは自分でやる必要がなくなったせいかあまり使わなくなりました。が、アクティブレンダリングでは使います。

    // ダブルバッファリング(db)用
    private Graphics dbg;
    private Image dbImage = null;

    /**
     * バッファにレンダリング(ダブルバッファリング)
     */
    private void gameRender() {
        // 初回の呼び出し時にダブルバッファリング用オブジェクトを作成
        if (dbImage == null) {
            // バッファイメージ
            dbImage = createImage(WIDTH, HEIGHT);
            if (dbImage == null) {
                System.out.println("dbImage is null");
                return;
            } else {
                // バッファイメージの描画オブジェクト
                dbg = dbImage.getGraphics();
            }
        }
        
        // バッファをクリアする
        dbg.setColor(Color.WHITE);
        dbg.fillRect(0, 0, WIDTH, HEIGHT);

        // ボールをバッファへ描画する
        ball.draw(dbg);
    }

まずバッファと呼ばれる領域を作ります。このバッファがdbImageです。dbImageは初めてgameRender()を呼び出したときに作成されます。あとdbImageに図や画像を書き込むのに必要なGraphicsオブジェクトdbgも初回に作られます。

以前は、paintComponent()の引数であるGraphicsオブジェクトgで描画してましたが、ダブルバッファリングではバッファ用のGraphicsオブジェクトdbgでバッファに対して描画します。

最後にバッファの内容を画面に転送するpaintScreen()です。

    /**
     * バッファを画面に描画
     */
    private void paintScreen() {
        try {
            // グラフィックオブジェクトを取得
            Graphics g = getGraphics();
            if ((g != null) && (dbImage != null)) {
                // バッファイメージを画面に描画
                g.drawImage(dbImage, 0, 0, null);
            }
            Toolkit.getDefaultToolkit().sync();
            if (g != null) {
                // グラフィックオブジェクトを破棄
                g.dispose(); 
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

画面専用のグラフィックオブジェクトは今まではpaintComponent()の引数Graphics gで自動的に作られてました。アクティブレンダリングでは自分で作る必要があります。これはgetGraphics()で取得できます。このgを使ってバッファ(dbImage)の内容をdrawImage()で描画するだけで画面に表示されます。あとこの方法で取得したgはdispose()で破棄する必要があります。

この手順をゲームループでまわすことによって1回のループ(フレーム)で必ず描画されるようになります。