2011年8月7日日曜日

とりあえずゲーム作り(5)

まずはプロジェクトから新規に作り直します。パッケージはJavaっぽく自分のサイトからとり、クラスもちゃんとファイル分けしていきます。


とりあえず2つのパッケージ、6つのクラスに整理しました。

まず基本となるのが、画面表示を担当する GameScreen クラス。個人的な好みで、仮想座標系にしました。実際とは異なる座標空間を用意して、画面に表示するときには変換する感じ。実際のスクリーンサイズを意識せずに開発できるかな、と。

仮想座標ですが、ある程度大きくて、かつ画面サイズの整数倍だと処理が楽です。そこでよくある画面サイズの最小公倍数を求め、

public static final int BASE_SIZE = 590284800;

と定義してみました。intなので9桁が妥当かな、と。コンストラクタは単純に以下のように用意します。乱数源のrand は初期化処理でよく使うので、ついでに用意。

public class GameScreen {
  int w, h;  // 仮想画面のサイズ
  Random rand;
  public GameScreen() {
    w = BASE_SIZE;
    h = BASE_SIZE;
    rand = new Random();
  }
}

シングルトン・パターン用のファクトリーも用意します。これは単なる趣味です。

static GameScreen singleton;
public static GameScreen factory() {
  return singleton == null ? (singleton = new GameScreen()) : singleton;
}

GameScreen はインスタンス生成しただけでは、あまり意味がありません。実際の画面サイズを教えてあげて、初期化しましょう。

int cw, ch;  // 実際の画面のサイズ
float xs, ys;  // 仮想と実際の画面サイズの比
public GameScreen init(Canvas c) {
  xs = cw = c.getWidth();
  ys = ch = c.getHeight();
  xs /= w;
  ys /= h;
  return this;
}

これで仮想画面と実際の画面の情報をもった GameScreen オブジェクトが出来上がりました。

なおこういった関数の戻り値は void で宣言することも多いとおもいますが、僕は基本的に関連オブジェクトを返すよう宣言するのが好みです。むろん、サイズや処理速度に制限がある場合には省きますが。

さて、この仮想画面上に配置される GameObject を定義してみましょう。

public class GameObject {
  public int w, h;  // 仮想画面上の大きさ
  public int x, y, z;  // 仮想画面上の位置
  public Rect r;  // 衝突判定用のRect
  public GameObject(int _w, int _h) {
    r = new Rect();
    init(_w, _h);
  }
}

これは見たまんま、あまり説明の必要がなさそうですね。で、別に初期化関数init()を用意します。コンストラクタでの初期化を最低限にして別に初期化関数を用意するのは、生成したインスタンスの再利用するための定番の構成だとおもいます。

  public int dx, dy;  // 移動スピード
  public int dxm, dym;  // 移動スピードの最大
  public int rpw, rph;  // 衝突判定エリアのPadding
  void init(int _w, int _h) {
    w = _w;
    h = _h;
    x = y = z = dx = dy = rpw = rph = 0;
    dxm = _w / 2;
    dym = _h / 2;
    calc();
  }
  public GameObject calc() {
    r.left = x + rpw;
    r.top = y + rph;
    r.right = x + w - rpw * 2;
    r.bottom = y + h - rph * 2;
    return this;
  }

まあ、移動スピードと衝突範囲を実装した、ベタなオブジェクト化って感じですね。衝突判定エリアはRectで実装してあります。これは移動するたび更新が必要ですから、calc()という関数に分離して他からも呼べるようにしています。

ちなみに今回のゲームでは、z軸方向の動きは使用しません。なのでzは0で固定として、処理は省略しています。将来的に必要となったら、拡張するとしましょうw

さて、GameScreenとGameObjectを関連付けていきましょう。まずはGameObjectに実画面に対応した座標情報を持たせてあげます。

  public int cw, ch;  // 実画面上の大きさ
  public int cx, cy;  // 実画面上の位置

この座標を扱うためにGameScreenのほうに幾つか関数を追加します。

  public int scaleX(int v) {  // x軸方向の座標変換
    return (int)(v * xs);
  }
  public int scaleY(int v) {  // y軸方向の座標変換
    return (int)(v * ys);
  }
  public GameObject transform(GameObject o) {
    o.cx = scaleX(o.x);
    o.cy = scaleY(o.y);
    return o;
  }
  public GameObject setup(GameObject o) {
    o.cw = scaleX(o.w);
    o.ch = scaleY(o.h);
    return transform(o);
  }

setup()は大きさも含めて設定してくれる関数で、今回はGameObjectの大きさは変化しないので最初に一回だけ呼べばOKです。移動した後にtrasnform()を呼べば、実画面の表示座標を更新してくれます。

さて実画面の座標が設定されたところで、GameObjectに画面表示用の関数を設定してあげましょう。

  public GameObject draw(Canvas c) {
    Paint paint = new Paint();
    paint.setColor(Color.RED);
    c.drawRect(new Rect(cx, cy, cx + cw, cy + ch), paint);
    return this;
  }

draw()は抽象関数としておき、サブクラスで実装させるのがオブジェクト指向っぽいのですが。僕の場合にはテスト目的で仮ロジックを記述してしまうことがおおいですね。コードサイズ的に問題がある場合には後で削除しちゃえば良いですし。

さて、実際に表示されるオブジェクトには画像を使用しますから、画像を扱えるようにGameObjectを拡張しておきましょう。

public class GameBitmapObject extends GameObject {
  public Bitmap bitmap;
  Rect br;
  public GameBitmapObject(int _w, int _h, Bitmap b) {
    super(_w, _h);
    br = new Rect(0, 0, b.getWidth(), b.getHeight());
    bitmap = b;
  }
  public GameObject draw(Canvas c) {
    c.drawBitmap(bitmap, br, new Rect(cx, cy, cx + cw, cy + ch), null);
    return this;
  }
}

コントラクタで画像をもらい、表示部分でそれを使っているだけ。このクラスは非常にシンプルに実装できましたね~

さて、ここまで作成したクラスを使用して、画面に何か表示させてみましょうか。まずは画面を初期化して

GameScreen gs = GameScreen.factory().init(canvas);

正方形のオブジェクトを1つ作りましょう。

GameObject go = new GameObject(10000, 10000);
gs.setup(go);

あとは表示させるだけですね。x, yの初期値は0なので、画面左上に赤い四角が表示されるはずです。

go.draw(canvas);

仮想座標上で右に10000ドット動かして表示してみましょう。

go.x += 10000;
gs.transform(go);  // 実座標を更新
go.draw(canvas);

ちなみにドロイド君が表示されるように変更したい場合、GameObject生成の1行を以下のように書き換えればOK。

Bitmap b = BitmapFactory.decodeResource(context.getResources(),R.drawable.droid);
GameBitmapObject go = new GameBitmapObject(10000, 10000, b);

とりあえず、これ使ってゲームを再構築していきます。そのために幾つかの便利関数を、それぞれのクラスに追加しておきましょう。

まずはGameObjectを動かす関数move()と、速度を変更する(加速させる)関数accelerate()を追加しておきます。最大速度を超えないようにしておきます。

  public GameObject move() {
    x += dx;
    y += dy;
    return this;
  }
  public GameObject accelerate(int x, int y) {
    dx += x;
    dx = dx < -dxm ? -dxm : dx > dxm ? dxm : dx;
    dy += y;
    dy = dy < -dym ? -dym : dy > dym ? dym : dy;
    return this;
  }

またGameObject同士の衝突を判定する関数collision()も追加しておきましょう。Rectクラスのcontains()は衝突判定として甘い感じ(相手が完全に含まれないとtrueにならない)なので、別途用意してみました。

  public Boolean collision(GameObject o) {
    return r.contains(o.r.left, o.r.top) || r.contains(o.r.left, o.r.bottom) || r.contains(o.r.right, o.r.top) || r.contains(o.r.right, o.r.bottom);
  }

GameScreenのほうにも追加しておきましょう。仮想座標を飛び出したGameObjectを逆側にワープさせる関数warp()は必要ですね。

  public GameObject warp(GameObject o) {
    o.x = warpCheck(o.x, o.w, w);
    o.y = warpCheck(o.y, o.h, h);
    return o;
  }
  static int warpCheck(int x, int w, int sw) {
    return x > sw ? -w : x < -w ? sw : x;
  }

また座標の初期化をする際に画面の中心に設定するcenter()と、画面上のランダムな位置に配置するrandom()はあると便利です。

  public GameObject center(GameObject o) {
    o.x = (w - o.w) / 2;
    o.y = (h - o.h) / 2;
    return o;
  }
  public GameObject random(GameObject o) {
    o.x = rand.nextInt(w - o.w);
    o.y = rand.nextInt(h - o.h);
    return o;
  }

また仮想座標系での大きさを得るため、以下の関数を用意してみました。例えばw(0.1F)は仮想画面の横幅の10%の長さを返します。

  public int w(float v) {
    return (int)(v * w);
  }
  public int h(float v) {
    return (int)(v * h);
  }

確保しておいた乱数を利用する手段も提供しておきましょうか。

  public int i(int r, int d) {
    return rand.nextInt(r) + d;
  }

さて、こんなものですかね。次回はこれらを使用し、実際にActivityを書き直していきましょう。

0 件のコメント:

コメントを投稿