人工知能に関する断創録

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

マップスクロール

マップが大きくなったとき画面がスクロールするようにします。横スクロールも縦スクロールも基本は同じなので横スクロールのみ扱います。RPGのスクロールとほとんど同じなんですがもっといいやり方がわかったので詳しく解説します。

mariolike04.jar

オフセットを使ったスクロール

マップを大きくすると画面上に全マップが収まらないためスクロール処理を実装する必要があります。図示すると下のようになります。

f:id:aidiary:20091017222715g:plain

スクロールの基本は非常にシンプルです。マップ全体の青色で囲んだ部分を切り出してスクリーンに表示させればいいわけです。注意点は

  • プレイヤーの移動にともなって青い枠は移動する。
  • プレイヤーは青い枠の中央に配置する。
  • ただし、マップの端ではプレイヤーは中央に配置しない。

くらいです。プログラムを実行してみるとすぐわかります。

スクロールは描画の問題なのでMainPanelクラスのpaintComponent()に実装しています。コードを見ていきます。

    /**
     * 描画処理
     * 
     * @param 描画オブジェクト
     */
    public void paintComponent(Graphics g) {
        ・・・

        // X方向のオフセットを計算
        int offsetX = MainPanel.WIDTH / 2 - (int)player.getX();
        // マップの端ではスクロールしないようにする
        offsetX = Math.min(offsetX, 0);
        offsetX = Math.max(offsetX, MainPanel.WIDTH - Map.WIDTH);

        ・・・

        // マップを描画
        map.draw(g, offsetX, offsetY);

        // プレイヤーを描画
        player.draw(g, offsetX, offsetY);
    }

スクロールはオフセットを使うと簡単に実装できます。オフセットはマップ全体のうちスクリーンに表示する開始位置を表しています(図を参照)。上の図ではマップ全体のうち-offsetXから書き始めればいいですね。

コードでは、オフセットを計算して、そのオフセット値をもとにしてマップやプレイヤーを描いています。オフセット値の計算式を見るとわかりますがプレイヤーがマップの中央付近にいるときはオフセットはマイナス値になります。

次にオフセット値をもとにどうマップを描画するかです。Mapクラスのdraw()を見てください。

    /**
     * マップを描画する
     * 
     * @param g 描画オブジェクト
     * @param offsetX X方向オフセット
     * @param offsetY Y方向オフセット
     */
    public void draw(Graphics g, int offsetX, int offsetY) {
        // オフセットを元に描画範囲を求める
        int firstTileX = pixelsToTiles(-offsetX);
        int lastTileX = firstTileX + pixelsToTiles(MainPanel.WIDTH) + 1;
        // 描画範囲がマップの大きさより大きくならないように調整
        lastTileX = Math.min(lastTileX, COL);

        ・・・

        g.setColor(Color.ORANGE);
        for (int i = firstTileY; i < lastTileY; i++) {
            for (int j = firstTileX; j < lastTileX; j++) {
                // mapの値に応じて画像を描く
                switch (map[i][j]) {
                    case 1 : // ブロック
                        g.fillRect(tilesToPixels(j) + offsetX, 
                                   tilesToPixels(i) + offsetY, 
                                   TILE_SIZE, TILE_SIZE);
                        break;
                }
            }
        }
    }

まずマップ全体のうちどこからどこまでをスクリーンに描画するか決めています。先ほどの図の青い範囲を描画したいわけですから、青い範囲の左端と右端の座標を求めています。それが、firstTileXとlastTileXです。このときlastTileXに+1してる点に注意してください。スクリーン上には表示されないですが少し大きめに描画してるわけです。なぜこうしなくちゃいけないかは+1を取って実行してみればすぐわかります。

ブロックを描画するときもoffsetXを足しています。これはマップの青い範囲の左端がスクリーン上のX=0にあたるためです。何度も言ってますがoffsetXはマイナス値なので足せば引かれます。

プレイヤーの描画も同じです。オフセットを加えるだけです。

    /**
     * プレイヤーを描画
     * 
     * @param g 描画オブジェクト
     * @param offsetX X方向オフセット
     * @param offsetY Y方向オフセット
     */
    public void draw(Graphics g, int offsetX, int offsetY) {
        g.setColor(Color.RED);
        g.fillRect((int) x + offsetX, (int) y + offsetY, WIDTH, HEIGHT);
    }

画面の端にいるときは

次に画面の端にいる場合の処理です。これはマップの端の壁を厚くすれば特に実装する必要はないのですが今回は別のアプローチでいきます。端へ行くとスクロールが止まってプレイヤーが移動する(スクリーンの中央から移動する)ようにします。図で書くとこうです。

f:id:aidiary:20091017222717g:plain

プレイヤーが左端(MainPanel.WIDTH / 2より左)へ移動するとオフセットがマイナスからプラスになります。オフセットがプラスになったらスクロールさせずにプレイヤーが移動するようにさせたいわけです。この処理は実は簡単に

    // マップの端ではスクロールしないようにする
    offsetX = Math.min(offsetX, 0);

と書くことができます。もしオフセットがプラスになったら0にするわけです。オフセットを0にするとマップ描画やプレイヤー描画のオフセットがすべて0になるためスクロールは止まります。

今度は逆側にいる場合です。こっちは計算で求めましょう。プレイヤーの座標をxとするとx > Map.WIDTH - MainPanel.WIDTH / 2のときオフセットをMainPanel.WIDTH - Map.WIDTHに固定すればいいわけです。

f:id:aidiary:20091017222718g:plain

オフセットは、MainPanel.WIDTH / 2 - xで計算できるのでxに上の式を代入すると

 オフセット = MainPanel.WIDTH / 2 - x
            < MainPanel.WIDTH / 2 - (Map.WIDTH - MainPanel.WIDTH / 2)
            = MainPanel.WIDTH - Map.WIDTH

となります。よって、オフセットがMainPanel.WIDTH - Map.WIDTHよりも小さいとき、オフセットをMainPanel.WIDTH - Map.WIDTHに固定すればいいことがわかります。これを簡潔に書くと、

    offsetX = Math.max(offsetX, MainPanel.WIDTH - Map.WIDTH);

こうなるわけです。

今回は横スクロールしか解説してませんが、縦スクロールもほとんど同じです。Y方向のオフセット値を同じように求めれば簡単にできます。えっ?斜めスクロールはどうするかって?横と縦を実装すれば斜めは自動的に実装されてますのでご安心を。