2011年8月10日水曜日

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

さて、引き続きゲームのコード再構築です。今回はActivity関連。

まずGameOverActivityは、元のMyGameOverクラスそのままです。また最初に起動されるMainActivityも、以下のようにボタン表示するだけのめちゃ簡単版。

public class MainActivity extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setTitle("Space Trip Game - Start");
    Button button = new Button(this);
    button.setText("Start Game");
    button.setOnClickListener(new View.OnClickListener() {
      public void onClick(View v) {
        Intent intent = new Intent(MainActivity.this, GameActivity.class);
        startActivity(intent);
      }
    });
    setContentView(button);
  }
}

画面いっぱいのボタンという、清清しいまでの手抜きな開始画面となっちょりますw


さて、今回のキモとなるGameActivityをみていきましょう。まずはクラス定義と、entire lifetime 用の関数定義です。

public class GameActivity extends Activity {
  GameView view;
  Thread mainThread;
  @Override protected void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setTitle("Space Trip");
    view = new GameView(this);
    setContentView(view);
  }
  @Override protected void onDestroy() {
    super.onDestroy();
    if (mainThread != null && mainThread.isAlive()) {
      mainThread.interrupt();
    }
  }
}

まあ定番の処理ですよね。ゲームの主要部分はGameViewクラスにあることがわかります。

続いて foreground lifetime 用の関数も定義しておきます。GameViewクラスのisRunningメンバを設定して、ゲーム内時間を止めたりしているだけですね。

  protected void onResume() {
    super.onResume();
    view.isRunning = true;
  }
  protected void onPause() {
    super.onPause();
    view.isRunning = false;
  }

ここまではガチ定番処理な感じですねぇ。

さて、これからいよいよGameViewクラスの定義にはいります。GameActivityの内部クラスとして定義されています。まずはクラス定義とコンストラクタ。

class GameView extends SurfaceView implements Runnable {
  Context context;
  Paint paint = new Paint();
  Boolean isRunning;
  GameScreen s;
  List<GameObject> rocks = new ArrayList<GameObject>();
  GameBitmapObject ship;
  long score, loopCount;
  public GameView(Context c) {
    super(c);
    context = c;
    mainThread = new Thread(this);
    mainThread.start();
  }
}

ゲーム自体の初期化は別にあるので、コンストラクタはわりと定番っぽいコードですね。ゲームの本体はスレッドで実行されるrun()関数にあります。まずは擬似コードで表現してみましょう。

public void run() {
  画面表示ができるまで待つ
  ゲームの初期化
  while (true) {  // メインループ
    if (isRunning) {
      自機や岩を動かす
      doDraw(); // 自機や岩など画面表示
      衝突チェック  ⇒ 衝突でgameover()
      スコアを計算
      ループ回数に応じて岩を追加 ⇒ addRock()
    }
  }
}

さて実際のコードを見てみましょう。まずは「画面表示ができるまで待つ」の部分。Canvasをロックできるまでループして待つロジックになっています。

  Canvas canvas;
  do {
    sleep(100);
    canvas = getHolder().lockCanvas();
  } while (canvas == null);

次は「ゲームの初期化」部分。

  s = GameScreen.factory().init(canvas);
  getHolder().unlockCanvasAndPost(canvas);
  Bitmap b = BitmapFactory.decodeResource(context.getResources(),R.drawable.droid);
  ship = new GameBitmapObject(s.w(0.1F), s.h(0.1F), b);
  init();

自機のサイズは縦横共に画面の10%に設定しています。現時点ではアスペクト比などまったく考慮していないです。なので横長の画面だと、かなりデブった感じのAndroid君が表示されることになりますね。

オブジェクト生成以外の初期化処理は、ゲームオーバー後にも使用するのでinit()にまとめています。

  void init() {
    paint.setColor(Color.WHITE);  // スコアの表示色
    score = loopCount = 0;
    s.setup(ship);
    s.center(ship);  // 自機を中心に戻す
    ship.dx = ship.dy = 0;  // 自機の速度を0に戻す
    ship.rpw = s.w(0.01F);  // 自機の当たり判定を画像より狭める
    ship.rph = s.h(0.01F);
    rocks.clear();  // 岩を全て取り除く
    addRock();  // ゲーム開始時には3つ岩が追加されている
    addRock();
    addRock();
  }

自機は最初は速度がゼロで、画面中心に配置されます。当たり判定のPadding幅(rpw, rph)を1%にしていますから、それぞれ中心から8%ぐらいが当たり判定のあるエリアになります。

さて続いてはメインループの中を記述しましょう。「自機や岩を動かす」部分は以下のようになっています。

  ship.m(s).calc();
  for (GameObject rock : rocks) {
    rock.m(s).calc();
  }

GameObject の関数 m() の説明がまだですね。これは以下のように定義された便利関数です。

  public GameObject m(GameScreen s) {
    move();
    s.warp(this);
    s.transform(this);
    return this;
  }

「衝突チェック」は以下のようなコードです。説明の必要がないぐらいシンプルですねw

  for (GameObject rock : rocks) {
    if (ship.collision(rock)) {
      gameover();
      break;
    }
  }

「スコアを計算」「ループ回数に応じて岩を追加」も以下のような単純なコードです。速度が速いほど点数が増えやすくなっています。また10秒ごとに岩が増えていくのがわかります。

  score += Math.abs(s.scaleX(ship.dx)) + Math.abs(s.scaleX(ship.dy));
  if (rocks.size() -3  < ++loopCount / 100)
    addRock();

さて、これでメイン部分のコードは終わりです。後は呼び出される3つの関数(doDraw, addRock, gameover)が残っているだけになりました。

doDraw()はGameObjectのdraw()を利用しているので、以下のようにシンプルです。

  void doDraw() {
    Canvas canvas = getHolder().lockCanvas();
    if (canvas != null) {
      canvas.drawColor(Color.BLACK);
      String sc = "0000000000" + score;
      canvas.drawText(sc.substring(sc.length() - 10), 0, paint.getTextSize(), paint);
      ship.draw(canvas);
      for (GameObject rock : rocks) {
        rock.draw(canvas);
      }
      getHolder().unlockCanvasAndPost(canvas);
    }
  }

addRock()は少し長いですが、GameBitmapObjectを定義してランダムに配置しているだけです。

  void addRock() {
    Bitmap b;
if (rocks.size() > 0)
      b = ((GameBitmapObject)rocks.get(0)).bitmap;
    else
      b = BitmapFactory.decodeResource(context.getResources(), R.drawable.redrock0);
    GameBitmapObject o = new GameBitmapObject(s.w(0.01F * s.i(10, 5)), s.h(0.01F * s.i(10, 5)), b);
    o.rpw = s.w(0.01F);
    o.rph = s.h(0.01F);
    s.setup(o);
    do {
      s.random(o);
      o.calc();
    } while (ship.collision(o));
    o.dx = s.w(0.001F * s.i(80, -39));
    o.dy = s.h(0.001F * s.i(80, -39));
    rocks.add(o);
  }

岩の画像は毎回生成せずに、2個目からは最初の岩の画像を使いまわすようにしています。サイズは縦横それぞれ、画面の5~15%ってとこでしょうか。自機と同様に画像より小さめの当たり判定を設定しています。

初期位置ですが、random()で配置した後に、collision()で自機との当たり判定をチェックしているあたり、前回と同じですね。岩追加と同時にゲームオーバーになるのを防いでいます。

最後はgameover()ですが、これは前回のものとほぼ同じでしょうかね。

  public void gameover() {
    isRunning = false;
    Intent intent = new Intent(GameActivity.this, GameOverActivity.class);
    intent.putExtra("SCORE", score);
    startActivity(intent);
    init();
  }

さて、これでコードが揃いました。実行してみましょう。


ほら、前のと同じゲームがプレイできるようになりました。仮想画面を使っているせいか、前より動きがなめらかな気がします。また岩が画像になっただけで、雰囲気が良くなりましたねぇ。

ただ衝突判定をきちんとした結果、ゲーム自体は前より難しくなったみたいですw


岩は時間で増えるので、最初におもいきって速度出して点数を稼いでおくのがコツかなぁ。現時点でも、そこそこ面白いですw

とりあえず動作するので、パッケージを公開してみます。興味ある方はご参照ください ⇒ SpaceTrip_20110810.zip

0 件のコメント:

コメントを投稿