Step08 状態遷移、固定小数点


#1

今回は知っておくと便利なテクニックを2つお知らせします。

■状態遷移

キャラクタの状態管理、ゲームの状態(シーン)管理に便利です。
早速、簡単な疑似コードを見ていきましょう。

2Dアクションゲームのキャラクタ移動を想像して読んでください。

void ChrExec(void)
{
    ChrExecX();
    ChrExecY();
}

void ChrExecX(void)
{
    // 左ボタン、右ボタンの入力を取得
    // 左ボタン、右ボタンに合わせて移動

    // キャラクタが画面外なら画面内に戻す
}

void ChrExecY(void)
{
    switch(state)
    {
    case CHR_STATE_LAND: ChrExecLand(); break;
    case CHR_STATE_JUMP: ChrExecJump(); break;
    case CHR_STATE_FALL: ChrExecFall(); break;

    default:
        // ERROR処理
        break;
    }
}

// 地面にいる場合
void ChrExecLand(void)
{
    if(Bボタン押されいない?)
    {
        return;
    }

    fmy = ジャンプの初期値;
    state = CHR_STATE_JUMP;
}

// ジャンプ中
void ChrExecJump(void)
{
    fmy += 重力分;
    fy  += fmy;

    if(fmy > 0)
    {
        state = CHR_STATE_FALL;
    }
}

// 落下中
void ChrExecFall(void)
{
    fmy += 重力分;
    fy  += fmy;

    if(着地した?)
    {
        fy  = 地面の座標;
        fmy = 0;

        state = CHR_STATE_LAND;
    }
}

チュートリアルの最後に動作するコードを載せます。

■固定小数点

Arduboyの場合、グローバル変数やローカル変数に余裕がありません。
float(4バイト)、double(4バイト)では大きすぎるため、2バイト変数を使います。

ただ単純な2バイトだと小数点計算が考慮されないので少々の工夫を加えるわけです。

たとえば0x0000という16進数を見たとき、これは4bitの塊が4つあると言えます。
0x0001、0x0002・・・と1つづつ加算していって、桁が上がるのは16番目の0x0010です。
そこで

上位12ビット = 整数
下位 4ビット = 小数点以下

という区分けを行います。シフト演算を使い、
コード上では計算処理、表示処理に分けて使います。

#define NUM2FIX(N)            ((N) << 4)
#define FIX2NUM(F)            ((F) >> 4)

// 計算
Ship.fx = NUM2FIX(OLED_SCREEN_CX/2 - SHIP_CX/2);
Ship.fy = 0;

// 表示
OledDrawBmp(FIX2NUM(Ship.fx), FIX2NUM(Ship.fy));

説明ではシフトするビットは4としましたが、
最大値、最小値を考えて7あたりが丁度いいように思います。

■サンプルコード

まとめです。

#include <Arduboy.h>

#define NUM2FIX(N)      ((N) << 7)
#define FIX2NUM(F)      ((F) >> 7)

#define CHR_CX          8
#define CHR_CY          8
#define SCREEN_CX       WIDTH
#define SCREEN_CY       HEIGHT

Arduboy ab;

enum {
    CHR_STATE_LAND,
    CHR_STATE_JUMP,
    CHR_STATE_FALL,
};

int16_t fx;
int16_t fy;
int16_t fmx;
int16_t fmy;
uint8_t state;

void ChrInit()
{
    fx  = NUM2FIX(SCREEN_CX/2 - CHR_CX/2);
    fy  = NUM2FIX(SCREEN_CY - CHR_CY);
    fmx = 0;
    fmy = 0;

    state = CHR_STATE_LAND;
}

void ChrDraw()
{
    ab.drawRect(FIX2NUM(fx), FIX2NUM(fy), CHR_CX, CHR_CY, 1);
}

void ChrExec()
{
    ChrExecX();
    ChrExecY();
}

void ChrExecX()
{
    if(ab.pressed(RIGHT_BUTTON))
    {
        fmx += 10;
    }

    if(ab.pressed(LEFT_BUTTON))
    {
        fmx -= 10;
    }

    if(fmx != 0)
    {
        if(fmx > 0) fmx--;
        else        fmx++;
    }

    fx += fmx;

    if(FIX2NUM(fx)+CHR_CX >= SCREEN_CX)
    {
        fx  = NUM2FIX(SCREEN_CX - CHR_CX);
        fmx = 0;
    }

    if(FIX2NUM(fx) < 0)
    {
        fx  = FIX2NUM(0);
        fmx = 0;
    }
}

void ChrExecY()
{
    switch(state)
    {
    case CHR_STATE_LAND: ChrExecLand(); break;
    case CHR_STATE_JUMP: ChrExecJump(); break;
    case CHR_STATE_FALL: ChrExecFall(); break;

    default:
        // 省略
        break;
    }
}

void ChrExecLand()
{
    if(!ab.pressed(B_BUTTON))
    {
        return;
    }

    fmy   = -600;
    state = CHR_STATE_JUMP;
}

void ChrExecJump()
{
    fmy += 30;
    fy  += fmy;

    if(fmy > 0)
    {
        state = CHR_STATE_FALL;
    }
}

void ChrExecFall()
{
    fmy += 30;
    fy  += fmy;

    if(FIX2NUM(fy)+CHR_CY >= SCREEN_CY)
    {
        fy  = NUM2FIX(SCREEN_CY - CHR_CY);
        fmy = 0;

        state = CHR_STATE_LAND;
    }
}


void setup()
{
    ab.beginNoLogo();
    ab.setFrameRate(60);

    ChrInit();
}

void loop()
{
    if(!(ab.nextFrame()))
    {
        return;
    }

    ChrDraw();
    ChrExec();

    ab.display();
    ab.clear();
}