人工知能に関する断創録

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

スクロール処理2

スクロール処理1(2005/10/10)の続きです。前今回はピクセル単位の移動を実装してドラクエっぽいスクロールを実現します。かくかくした移動ではなくて、滑らかな移動になります。イメージがわかない方はプログラムを実行してみてください。

rpg10.jar

ゲームループの変更

大雑把なところから順に見ていきます。MainPanelのゲームループであるrun()を見てください。

    public void run() {
        while (true) {
            // キー入力をチェックする
            checkInput();

            // 移動(スクロール)中なら移動する
            if (hero.isMoving()) {
                if (hero.move()) {  // 移動(スクロール)
                    // 移動が完了した後の処理はここに書く
                }
            }

            repaint();

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

前回とだいぶ変わっています。勇者(hero)の移動は、前回までhero.move(LEFT)のように単純でしたが少し複雑になってます。前回までは方向キーを押した瞬間にとなりのマスに移動すればよかったので1回のループで1マスの移動処理が完了してました。しかし、今回からは方向キーを1回押したあと数ピクセルごとに徐々に移動するようにしたいわけです。つまり、ループが何回か回ってようやく1マス移動することになります。具体的には1回のループで2ピクセル(Chara.SPEED)ごと移動するようにしてます。1マスは16ピクセル(Common.CS)ですので 8回ループが回ると1マス移動が完了します。

    // 移動(スクロール)中なら移動する
    if (hero.isMoving()) {
       if (hero.move()) {  // 移動(スクロール)
            // 移動が完了した後の処理はここに書く
       }
    }

まず、方向キーを押して移動を開始するとChara.isMovingがtrueになります。isMovingはピクセル単位の移動中のとき true、静止しているときfalseになります。移動中はmove()メソッドを呼び出して実際に移動します。前回と違ってmove()はピクセル単位の移動です。つまり、move()を1回呼び出すと2ピクセル(Chara.SPEED)だけ移動します。move(LEFT)のように方向の引数を渡してないことに注意してください。勇者が向いている方向はCharaクラスのdirectionでわかるので省略できます。move()は1マス(16ピクセル)移動が完了した場合trueを返し、1マス移動が完了してない場合はfalseを返します。上のようにif文で囲んでやれば1マス移動が完了した場合の処理(毒沼でダメージを受けるなど)を追加できます。次にcheckInput()を見てみます。左キーが押された場合だけ載せます。後はほとんど同じです。

    private void checkInput() {
        if (leftKey.isPressed()) { // 左
            if (!hero.isMoving()) {       // 移動中でなければ
                hero.setDirection(LEFT);  // 方向をセットして
                hero.setMoving(true);     // 移動(スクロール)開始
            }
        }
        ・・・
    }

左キーが押されている(leftKey.isPressed()がtrue)とき、勇者が移動中でなければ移動を開始しています。移動中のとき(hero.isMoving()がtrue)はキーを押しても方向転換できないことに注意してください。1マス(16ピクセル)移動が完了するまでは方向転換できないようにしています。

移動を開始するには、setDirection()で進む方向をセットしてからsetMoving()でisMovingをtrueにセットします。こうすると、ゲームループのhero.isMoving()がtrueになるためmove()で移動が始まります。

最後に、ゲームループの休止時間が20ミリ秒に変更してあります(前回は200ミリ秒でした)。前回までは1回のループで1マス移動していたのに対し、今回からは1回のループで2ピクセルしか移動しません。200ミリ秒も休止してたらすごくのろくなっちゃいます。試してみてください。

ピクセル単位移動

さらに詳細を見ていきます。Charaクラスのmove()です。呼び出されると向いている方向に2ピクセルだけ移動します。

    /**
     * 移動処理。 
     * @return 1マス移動が完了したらtrueを返す。移動中はfalseを返す。
     */
    public boolean move() {
        switch (direction) {
            case LEFT:
                if (moveLeft()) {
                    // 移動が完了した
                    return true;
                }
                break;
            case RIGHT:
                if (moveRight()) {
                    // 移動が完了した
                    return true;
                }
                break;
            case UP:
                if (moveUp()) {
                    // 移動が完了した
                    return true;
                }
                break;
            case DOWN:
                if (moveDown()) {
                    // 移動が完了した
                    return true;
                }
                break;
        }
        
        // 移動が完了していない
        return false;
    }

移動する方向によってさらにmoveLeft()、moveRight()、moveUp()、moveDown()を呼び出してます。似たようなメソッドなので1つにまとめられるかもしれませんがわかりやすさ優先で分けました。moveLeft()だけ見てみます。

    private boolean moveLeft() {
        // 1マス先の座標
        int nextX = x - 1;
        int nextY = y;
        if (nextX < 0) nextX = 0;
        // その場所に障害物がなければ移動を開始
        if (!map.isHit(nextX, nextY)) {
            // SPEEDピクセル分移動
            px -= Chara.SPEED;
            if (px < 0) px = 0;
            // 移動距離を加算
            movingLength += Chara.SPEED;
            // 移動が1マス分を超えていてたら
            if (movingLength >= CS) {
                // 移動する
                x--;
                if (x < 0) x = 0;
                px = x * CS;
                // 移動が完了
                isMoving = false;
                return true;
            }
        } else {
            isMoving = false;
        }
        
        return false;
    }

移動先のマスを(nextX, nextY)とします。その座標に障害物がなければ移動を開始します。移動は2ピクセル(Chara.SPEED)だけです。(x,y)ではなく、 (px,py)を変更している点に注意してください。(x,y)はマス単位の勇者の座標であるのに対し、(px,py)はピクセル単位の勇者の座標です。ここでは、2ピクセルだけ移動するので(px,py)を更新します。移動したピクセル数はmovingLengthに保存しておきます。1マス分(16ピクセル)移動したら勇者のマス単位の座標(x,y)を更新してます。あとはisMovingをfalseに設定して、1マス移動が完了したとのことでtrueを返します。