遥かへのスピードランナー

シリコンバレーでAndroidアプリの開発してます。コンピュータービジョン・3D・アルゴリズム界隈にもたまに出現します。

Box2DFlashAS3でブロック崩しゲームを作る

世の中は年が開けて抱負やら振り返りやらでとてもフレッシュな香りがただよっていますが、僕は空気も読まずにとりあえず年末年始の休暇にやってたことをまとめようと思います。

まずはBox2DFlashAS3という物理エンジンのライブラリを使ってブロック崩しゲームを作りました。
やっつけで作った糞ゲーもいいところですが、せっかくなので貼付けておきます。左右でバーを動かすことができます。床に玉をついてもゲームオーバーはありません。


Box2DFlashAS3(以下Box2D)とは

ActionScriptから利用できる物理エンジン、いわゆるニュートン力学が支配する世界を手軽に表現することが出来るライブラリの一つです。これを使うことで重力による落下とか衝突とかを簡単に実装することができます。
物理エンジンには様々なライブラリがあり、たとえばNVIDIAPHYSXなんかはよく知られています。2Dも3Dもありますが、Box2Dは2Dの物理エンジンの代表的なものです。僕は物理エンジンというもの自体になじみがなかったのですが、Web+DB PRESS vol.54の特集を読んで初めて知りました。

工夫した点など

あまり情報がないのですが、Box2Dの物体(b2Bodyオブジェクト)には作成した物体のIDや名前などのプロパティがないため、後から物体を一意に識別したい場合は、UserDataというカスタムフィールドを参照します。
以下はブロック崩しゲームにおける、ブロックの初期作成処理です。UserDataに、ブロックの連番であるindexプロパティと、ブロックであることを示すtypeプロパティをセットしています。

      for (var i:int = 0; i<boxNum; i++) {
        //中略
        var block:b2Body = world.CreateBody(blockBodyDef);
        var blockObj:Object = new Object();
        blockObj.index = i;
        blockObj.type = "block";
        //中略
        block.SetUserData(blockObj);
        //中略
      }

以下がUserDataを参照している部分。ContactListenerという衝突検知用のクラスの中で、UserDataのtypeプロパティを参照して、衝突した物体がブロックだったかどうかを判定しています。

    public override function Add(point:b2ContactPoint):void {
      var body1:b2Body = point.shape1.GetBody();
      var body2:b2Body = point.shape2.GetBody();
      var obj1:Object = body1.GetUserData();
      var obj2:Object = body2.GetUserData();
      if (obj1 != null && obj1.type == "block") {
        //中略
      } else if (obj2 != null && obj2.type == "block") {
        //中略
      }
    }

全コード

BlockGame.as

package  {
  import Box2D.Collision.b2AABB;
  import Box2D.Collision.b2ContactPoint;
  import Box2D.Collision.Shapes.b2PolygonDef;
  import Box2D.Collision.Shapes.b2CircleDef;
  import Box2D.Collision.Shapes.b2MassData;
  import Box2D.Common.Math.b2Vec2;
  import Box2D.Dynamics.b2Body;
  import Box2D.Dynamics.b2BodyDef;
  import Box2D.Dynamics.b2ContactListener;
  import Box2D.Dynamics.b2DebugDraw;
  import Box2D.Dynamics.b2World;
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.events.MouseEvent;
  import flash.events.KeyboardEvent;
  import flash.ui.Keyboard;
  import flash.text.TextField;
  import flash.text.TextFieldAutoSize;
  import flash.text.TextFormat;

  public class BlockGame extends Sprite {
    private var world:b2World;
    private var barstate:int;
    private var bar:b2Body;
    public var blockArray:Array;
    public var destroyFlagArray:Array;
    private var destroyNum:int;
    private var textField:TextField;
    private const boxNum:int = 35;
    
    public function BlockGame():void {
      // イベントハンドラを登録する
      barstate = 0;
      textField = new TextField();
      // テキストフィールドの準備
      textField.autoSize = TextFieldAutoSize.LEFT;
      textField.textColor = 0xFFFFFF;
      textField.x = 100;
      textField.y = 220;
      textField.text = "Click to start.";
      var f:TextFormat = new TextFormat();
      f.font="Arial";
      f.size=24;
      textField.setTextFormat(f);
      addChild(textField);
      stage.addEventListener(MouseEvent.CLICK, clickHandler);
      stage.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
      stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler);
      stage.addEventListener(KeyboardEvent.KEY_UP, keyUpHandler);
    }
    
    private function clickHandler(event:MouseEvent):void {

      trace("start clickHandler");

      destroyNum = 0;
      textField.text = "";

      ////////////////////////////////////////
      // 物理エンジンのセットアップ
      
      var worldAABB:b2AABB = new b2AABB();
      worldAABB.lowerBound.Set(-100, -100);
      worldAABB.upperBound.Set(100, 100);
      var gravity:b2Vec2 = new b2Vec2(0, 10);
      world = new b2World(worldAABB, gravity, true);
      
      ////////////////////////////////////////
      // 固定オブジェクトの配置

      var floor:b2Body = createStaticBoxShape(2.5,  3.0, 2.3,  0.1);
      var top:b2Body   = createStaticBoxShape(2.5, -0.1, 2.3,  0.1);
      var l:b2Body     = createStaticBoxShape(0.1,  3.0, 0.1, 50.0);
      var r:b2Body     = createStaticBoxShape(4.9,  3.0, 0.1, 50.0);
      
      ///////////////////////////////////////
      // ユーザーが動かすバーを作成する

      var barBodyDef:b2BodyDef = new b2BodyDef();
      barBodyDef.position.Set(2.5, 2.8);
      var barShapeDef:b2PolygonDef = new b2PolygonDef();
      barShapeDef.SetAsBox(0.5, 0.1);
      barShapeDef.density = 1;
      barShapeDef.restitution = 0;
      bar = world.CreateBody(barBodyDef);
      bar.CreateShape(barShapeDef);
      bar.SetMassFromShapes();

      blockArray = new Array();
      destroyFlagArray = new Array();
      var bw:int = 7;
      for (var i:int = 0; i<boxNum; i++) {
        var x:int = i%bw;
        var y:int = i/bw;
        var xd:Number = 0.7 + x * 0.6;
        var yd:Number  = 0.2 + y * 0.2;
        var blockBodyDef:b2BodyDef = new b2BodyDef();
        blockBodyDef.position.Set(xd, yd);
        var blockShapeDef:b2PolygonDef = new b2PolygonDef();
        blockShapeDef.SetAsBox(0.3, 0.1);
        var block:b2Body = world.CreateBody(blockBodyDef);
        var blockObj:Object = new Object();
        blockObj.index = i;
        blockObj.type = "block";
        blockObj.destroy = false;
        block.SetUserData(blockObj);
        block.CreateShape(blockShapeDef);
        blockArray.push(block);
        destroyFlagArray.push(false);
      }
      
      ////////////////////////////////////////
      // ボールの設置
      
      var ballBodyDef:b2BodyDef = new b2BodyDef();
      ballBodyDef.position.Set(2.5, 1.0);     
      var ballShapeDef:b2CircleDef = new b2CircleDef();
      ballShapeDef.radius = 0.05;
      ballShapeDef.density = 0.2;        // 密度 [kg/m^2]
      ballShapeDef.restitution = 1;  // 反発係数、通常は0〜1
      var ballBody:b2Body = world.CreateBody(ballBodyDef);
      ballBody.CreateShape(ballShapeDef);
      ballBody.SetMassFromShapes();
      ballBody.ApplyImpulse(new b2Vec2(0.01, 0.01), ballBody.GetWorldCenter().Copy());

      var contactListener:ContactListener = new ContactListener();
      world.SetContactListener(contactListener);

      ////////////////////////////////////////
      // 描画設定
      
      var debugDraw:b2DebugDraw = new b2DebugDraw();
      debugDraw.m_sprite = this;
      debugDraw.m_drawScale = 100; // 1mを100ピクセルにする
      debugDraw.m_fillAlpha = 0.3; // 不透明度
      debugDraw.m_lineThickness = 1; // 線の太さ
      debugDraw.m_drawFlags = b2DebugDraw.e_shapeBit;
      world.SetDebugDraw(debugDraw);
    }

    private function keyDownHandler(event:KeyboardEvent):void {
      if (event.keyCode == Keyboard.LEFT) {
        barstate = 1;
        stage.addEventListener(Event.ENTER_FRAME, enterFrameHandler2);
      } else if (event.keyCode == Keyboard.RIGHT) {
        barstate = 2;
        stage.addEventListener(Event.ENTER_FRAME, enterFrameHandler2);
      }
    }

    private function keyUpHandler(event:KeyboardEvent):void {
      if (barstate == 1 && event.keyCode == Keyboard.LEFT) {
        barstate = 0;
        stage.removeEventListener(Event.ENTER_FRAME, enterFrameHandler2);
      } else if (barstate == 2 && event.keyCode == Keyboard.RIGHT) {
        barstate = 0;
        stage.removeEventListener(Event.ENTER_FRAME, enterFrameHandler2);
      }
    }

    private function enterFrameHandler2(event:Event):void {
      var c:b2Vec2 = bar.GetWorldCenter().Copy();
      var f:b2Vec2;
      if (barstate == 1) {
        f = new b2Vec2(-1,0);
      } else if (barstate == 2) {
        f = new b2Vec2(1,0);
      }
      bar.ApplyForce(f, c);
    }

    private function enterFrameHandler(event:Event):void {
      if (world == null) {
        return;
      }
      if (destroyNum == boxNum) {
        gemeEnd();
        return;
      }

      for (var i:int=0; i<boxNum; i++) {
        if (!destroyFlagArray[i]) {
          var obj:Object = blockArray[i].GetUserData();
          if (obj && obj.destroy) {
            trace("destory:"+i);
            world.DestroyBody(blockArray[i]);
            destroyFlagArray[i] = true;
            ++destroyNum;
          }
        }
      }
      world.Step(1/24, 10);
    }

    private function gemeEnd():void {
      textField.text = "Clear! Click to restart.";
      world = null;
    }

    private function createStaticBoxShape(locX:Number, locY:Number, sizeX:Number, sizeY:Number):b2Body {
      var bbd:b2BodyDef = new b2BodyDef();
      bbd.position.Set(locX, locY);
      var bpd:b2PolygonDef = new b2PolygonDef();
      bpd.SetAsBox(sizeX, sizeY);
      var b:b2Body = world.CreateBody(bbd);
      b.CreateShape(bpd);
      return b;
    }
  }
}

ContactListener.as

package  {
  import Box2D.Collision.b2ContactPoint;
  import Box2D.Collision.Shapes.b2MassData;
  import Box2D.Dynamics.b2ContactListener;
  import Box2D.Dynamics.b2Body;
  import Box2D.Collision.Shapes.b2PolygonDef;
  
  public class ContactListener extends b2ContactListener {
    
    public function ContactListener() {
    }
    
    public override function Add(point:b2ContactPoint):void {
      var body1:b2Body = point.shape1.GetBody();
      var body2:b2Body = point.shape2.GetBody();
      var obj1:Object = body1.GetUserData();
      var obj2:Object = body2.GetUserData();
      if (obj1 != null && obj1.type == "block") {
        trace("this is obj1");
        obj1.destroy = true;
      } else if (obj2 != null && obj2.type == "block") {
        trace("this is obj2");
        obj2.destroy = true;
      }
    }
  }
}

その他補足情報

コンパイルするときは、BlockGame.asとContactListener.asを同じディレクトリにおいて、

mxmlc -source-path=path_to_box2d_library BlockGame.as 

と実行(要Flex SDK、およびパス設定)。

traceで吐き出したログを参照する方法は
windowsやmacで、flashのtraceログが吐かれる場所 - カサヒラボ
あたりを参照。

次回は3Dの物理エンジンをいじった成果を書く予定です。