Cocos Code IDEとLua言語でブロック崩しを作ってみる

More than 1 year has passed since last update.

Cocos Code IDEを使ってLua言語で、簡単なブロック崩しを作ってみます。

実行環境、IDE Cocos2d-xバージョン 言語
Cocos Code IDE Mac OS X 1.0.0-RC2 Cocos2d-x V3.2 Lua言語

タップ&ドラッグでパドルを操作してボールを打ち返し、ブロックを壊していくブロック崩しを作ってみました。ボールが落下したら即ゲームオーバーです。ボールの動きは物理エンジンを利用してしまうので、反射等の部分はシステムまかせです。
コード数を少なくしたかったので、ゲームにはタイトル画面や、画像効果、音楽、効果音等はありません。音が無いと嫌だって方はサウンド再生の基本(Cocos Code IDE, Lua言語)を参考に、音をつけてみてください。

スクリーンショット 2014-09-23 17.09.33.png

Code IDEを使ってLua言語で作るゲーム制作はC++での制作と比べて、言語が扱いやすく、C++では時間のかかるビルドが無いため、お手軽にゲーム制作ができます。

これから趣味でスマートフォンのゲーム制作を始めようという方にはおすすめなのですが、情報がまだ少ないのが難点です。という私もLuaを使い出してまだ日が浅いのですが。

業務用ゲーム制作にどれだけ使えるのかわかりませんが、私のような個人レベルで開発をしている人、これから始める人というのはおそらく小規模なゲーム制作でしょうから、Code IDEとLuaの組み合わせはベストな選択なのではないでしょうか。

(Code IDEでのブロック崩しの作り方が中心ですので、Lua言語の説明は割愛していますのでご了承ください。)

最初の準備

最初に新規プロジェクトを作成します。

Code IDEを起動し、プロジェクトを新規作成で作成します。(FileメニューのNew->Cocos Lua Project...から作成できます。)
プロジェクト名は適当につけてください。BreakoutでもBlockKuzushiでも何でもかまいません。ちゃんとしたプロジェクト名にローマ字はふさわしくありませんが、テストなので気にせずに行きましょう。

名前をつけたら[Next]を押します。

次に、画面は縦画面(Orientation:でportraitを選択)を指定します。
[Finish]ボタンでプロジェクトが作成されます。

作成されたプロジェクトはサンプルプログラムです。起動すると背景が表示されてキャラクターが歩くプログラムが起動します。ここではまずそれを空の状態にしてからゲームを作成します。

main.luaを修正する

main.luaを開くといろいろコードが書いてあります。私も全部理解しているわけではありません。
詳細は説明は書きませんが(書けませんが?)主に修正を加えるのはGameScene.luaファイルの方ですので、必要最小限の修正を加えておきます。

まず、縦画面に対応するため、setDesignResolutionSizeの数値を変更します。次にその下の行にsetContentScaleFactorを加えますが、これは表示比率を変えるもので、2.0を指定すると、例えばiPhoneでのRetinaディスプレイのように高解像度で画像が表示されます。

main.lua
cc.Director:getInstance():getOpenGLView():setDesignResolutionSize(320, 480, cc.ResolutionPolicy.SHOW_ALL) -- (480, 320, 0)から修正
cc.Director:getInstance():setContentScaleFactor(2.0) -- 表示比率

ここでは音楽は鳴らしませんので以下の行をコメントアウトします。

main.lua
--gameScene:playBgMusic()

これで、main.luaの修正は終わりです。(Githubにあるファイルはこれより若干の変更を加えています。)

GameScene.luaを空にする

次にGameScene.luaを必要最小限の状態にします。今後このファイルにゲームのコードを記述していきます。GameScene.luaを以下のコードと置き換えてください。

GameScene.lua
require "Cocos2d"
require "Cocos2dConstants"

local GameScene = class("GameScene",function()
    return cc.Scene:createWithPhysics()
end)

function GameScene:ctor()
end

function GameScene.create()
    local scene = GameScene.new()
    return scene
end

-- レイヤーの作成
function GameScene:createLayer()
end

-- パドルの作成
function GameScene:createPaddle()
end

-- ボールの作成
function GameScene:createBall()
end

-- ラベルの作成
function GameScene:createLabel()
end

-- 画面の外周に壁を作る
function GameScene:createOuterWall()
end

-- 底面にミス判定用の線を作る
function GameScene:createBottomWall()
end

-- ブロックレイヤーの作成
function GameScene:createBlockLayer()
end

-- ブロックレイヤーの削除
function GameScene:removeBlockLayer()
end

-- ゲーム開始準備
function GameScene:readyToStart()
end

-- ゲーム開始
function GameScene:gameStart()
end

-- ゲームオーバー
function GameScene:gameOver()
end

-- ゲームクリアー
function GameScene:gameClear()
end

-- タッチイベント処理
function GameScene:touchEvent()
end

-- 衝突時に呼ばれるイベント処理
function GameScene:contactTest()
end

return GameScene

最初のGameScene = class・・・でクラスを作成しています。class関数内で、親クラスのインスタンスを返しています。元のGameScene.luaではcc.Scene:create()を返していますが、物理エンジンを使ったシーンを作成するため、cc.Scene:createWithPhysics()を返すように変更しています。

(Luaではクラスの機能が用意されているわけではなく、クラスのような機能をする関数をCode IDEで用意していて、それを使っています。Cocos2d-xのコードを覗けば詳しい動作を知ることもできますが、最初はC++のクラスと同じようなものと思って使って支障はないはずです。)

ctor()メソッドがC++でのコンストラクタにあたります。
create()でこのクラスのインスタンスを作成し、返しています。

create()メソッドの下にはこれから作成していくメソッドのひな形として、空のメソッドを複数記述しています。

ちなみにクラス名GameSceneとメソッド名の間がコロン(:)だったりピリオド(.)だったりしますが、クラスのメソッドとしてはコロン(:)を使います。
コロンとピリオドの違いについては説明が長くなりそうなので割愛します。今後別の記事で投稿するかもしれません。

画像を用意する

resディレクトリの中を空にします。ファイルを選択してDeleteキーで削除してください。(削除せずにそのままでも実行するには特に問題はありません。)

ball.png、blocks.png、paddle.pngの3つの画像を用意し、resディレクトリにドラッグ&ドロップでコピーします。
(画像ファイルはgithub.com/senchan-office/Breakoutにあります。)

試しにプロジェクトを実行してみてください。
エラーが出ずに、真っ黒な画面が表示されればOKです。
スクリーンショット 2014-09-23 17.09.58.png

ここまでで準備は終わりです。
いよいよゲームのコードを記述していきます。

ゲームのコードを記述する

コードとしてはそれほど複雑ではありません。

  • 定数やコンストラクタcreateメソッド等の準備
  • レイヤとそこに配置するボールなどのオブジェクトの作成
  • ゲーム開始、ゲームオーバーなど状態ごとの処理
  • タッチイベント、衝突イベントなどイベント処理

に大きく分かれます。

定数を記述する

コードの先頭にゲーム内で使用する定数を記述していきますが、Luaではconstなどが利用できないようなので、定数もどきにして記述していきます。

GameScene.lua
中略・・・
require "Cocos2dConstants"

local kBlockHorizontalCount = 8   -- 水平方向ブロック数
local kBlockVerticalCount   = 5   -- 垂直方向ブロック数
local kBlockWidth           = 40  -- ブロックの幅
local kBlockHeight          = 15  -- ブロックの高さ
local kTopMargin            = 30  -- 上部マージン
local kBottomMargin         = 30  -- 底部マージン
local kOuterBottomSize      = 50  -- 画面下部の領域のサイズ
local kOuterWallSize        = 100 -- 壁の厚さ
local kBallSpeed            = 700 -- ボールのスピード

-- 物理シミュレーションで使用するカテゴリ
local kCategoryPaddle       = 1    -- 00001
local kCategoryBall         = 2    -- 00010
local kCategoryBlock        = 4    -- 00100
local kCategoryBottom       = 8    -- 01000
local kCategoryWall         = 16   -- 10000

local GamePhase = {ReadyToStart = 1, Playing = 2, GameOver = 3, GameClear = 4}

コンストラクタにメンバ変数を記述する

コンストラクタにメンバ変数を記述していきます。変数をここで定義しなければいけないわけではなく、クラスのメソッドの中で、例えばself.ballなどをいきなり使っても動作してしまいます。
あくまで、わかりやすさのために、「この後こんな変数を使いますよ」という意味で書いています。

GameScene.lua
function GameScene:ctor()
    self.visibleSize = cc.Director:getInstance():getVisibleSize()
    self.gamePhase  = GamePhase.ReadyToStart
    self.blockCount = 10   -- 残りのブロック数
    self.layer      = nil  -- レイヤー
    self.paddle     = nil  -- パドルのスプライト
    self.ball       = nil  -- ボールのスプライト
    self.label      = nil  -- ラベル
    self.material = cc.PhysicsMaterial(0, 1, 0) -- 密度, 反発, 摩擦
    self.blockTexture = cc.Director:getInstance():getTextureCache():addImage("blocks.png")
end

createメソッドを記述する

コンストラクタに全部書いてしまってもかまわないのですが、サンプルプログラムにならって、createメソッドに処理を記述していきます。

GameScene.lua
function GameScene.create()
    local scene = GameScene.new()

    --scene:getPhysicsWorld():setDebugDrawMask(cc.PhysicsWorld.DEBUGDRAW_ALL) -- デバッグ用
    scene:getPhysicsWorld():setGravity(cc.p(0,0))

    scene.layer = scene:createLayer()
    scene:addChild(scene.layer)

    scene:touchEvent()
    scene:contactTest()
    scene:readyToStart()
    return scene
end

PhysicsWorldのsetDebugDrawMakの行のコメントを外すと物体の形状が表示されるようになります。動作がおかしい時などの確認によく使います。
PhysicsWorldのsetGravityメソッドでは(0,0)で無重力に設定しています。
createLayer()でメインとなるレイヤーを作成してシーンに加えています。
touchEvent()、contactTest()はそれぞれ画面タッチと物体衝突時のイベント処理を行い、readyToStart()でゲーム開始準備に入ります。

メインのレイヤーを作成する

メインのレイヤーを作成し、その上にパドル、ボール、ゲームオーバーなどの文字を表示するラベル、画面サイズの壁を配置していきます。

GameScene.lua
function GameScene:createLayer()
    local layer = cc.LayerColor:create(cc.c4b(0, 0, 255, 255)) -- 色は青、不透明

    self.paddle = self:createPaddle()
    self.ball = self:createBall()
    self.label = self:createLabel()

    layer:addChild(self.paddle)
    layer:addChild(self.ball)
    layer:addChild(self.label)

    layer:addChild(self:createOuterWall())
    layer:addChild(self:createBottomWall())

    return layer
end

パドル、ボールとラベルを作成する

次にパドル、ボールのスプライトと、「Game Over」などの文字表示用のラベルを作成していきます。
スプライトを作成して、物体としての形状や衝突判定の情報をPhysicsBodyとして作成して、スプライトに設定します。
(さらに詳しくは物理シミュレーション基本(Cocos Code IDE, Lua言語)物理シミュレーションで物体の衝突時の処理(Cocos Code IDE, Lua言語を参照してください。)

GameScene.lua
-- パドルの作成
function GameScene:createPaddle()
    local sprite = cc.Sprite:create("paddle.png")

    local physicsBody = cc.PhysicsBody:createBox(sprite:getContentSize(), self.material)
    physicsBody:setDynamic(false)
    physicsBody:setCategoryBitmask(kCategoryPaddle)

    sprite:setPhysicsBody(physicsBody)

    return sprite
end

-- ボールの作成
function GameScene:createBall()
    local sprite = cc.Sprite:create("ball.png")

    local physicsBody = cc.PhysicsBody:createCircle(sprite:getContentSize().width/2, self.material)
    physicsBody:setRotationEnable(false)
    physicsBody:setCategoryBitmask(kCategoryBall)
    physicsBody:setContactTestBitmask(kCategoryBlock + kCategoryBottom + kCategoryWall)

    sprite:setPhysicsBody(physicsBody)

    return sprite
end

-- ラベルの作成
function GameScene:createLabel()
    local label = cc.Label:createWithSystemFont("", "Arial", 18)
    label:setPosition(self.visibleSize.width/2, self.visibleSize.height/2)

    return label
end

周囲に壁を作成する

ボールが画面外に出ないように、画面の周囲に見えない外壁を用意します。今回は外壁をすり抜けないように厚みのある壁にしています。
(Cocos2d-x v3.2では物理エンジンにChipmunkを使っていて、小さくて速い物体は線をすり抜けることがあるようです。対処方法がわかりませんでしたので、壁に厚みをつけて対処しています。)

GameScene.lua
-- 画面の外周に壁を作る
function GameScene:createOuterWall()
    local node = cc.Node:create()
    node:setAnchorPoint(0.5, 0.5) -- アンカーポイントを設定しないとwarningが出る
    node:setPosition(self.visibleSize.width/2, self.visibleSize.height/2)

    -- 画面の外周に4つのボックスを作る
    local box1 = cc.PhysicsShapeBox:create(cc.size(self.visibleSize.width + kOuterWallSize*2, kOuterWallSize), self.material, cc.p(0, self.visibleSize.height/2 + kOuterWallSize/2))
    local box2 = cc.PhysicsShapeBox:create(cc.size(self.visibleSize.width + kOuterWallSize*2, kOuterWallSize), self.material, cc.p(0, -self.visibleSize.height/2 - kOuterWallSize/2 - kOuterBottomSize))
    local box3 = cc.PhysicsShapeBox:create(cc.size(kOuterWallSize, self.visibleSize.height + kOuterBottomSize), self.material, cc.p(-self.visibleSize.width/2 -  kOuterWallSize/2, -kOuterBottomSize/2))
    local box4 = cc.PhysicsShapeBox:create(cc.size(kOuterWallSize, self.visibleSize.height + kOuterBottomSize), self.material, cc.p(self.visibleSize.width/2 + kOuterWallSize/2, -kOuterBottomSize/2))
    local body = cc.PhysicsBody:create()
    body:addShape(box1)
    body:addShape(box2)
    body:addShape(box3)
    body:addShape(box4)
    body:setDynamic(false)
    body:setCategoryBitmask(kCategoryWall)
    body:setContactTestBitmask(kCategoryBall)

    -- ノードに上で作った物体を設定
    node:setPhysicsBody(body) 

    return node
end

ミス判定用の線を作る

ボールが画面下に落下した時にゲームオーバーとします。ゲームオーバの判定用に画面下に線を作り、ボールとこの線が接触したらゲームオーバーと判定しています。

GameScene.lua
-- 底面にミス判定用の線を作る
function GameScene:createBottomWall()
    local node = cc.Node:create()
    node:setAnchorPoint(0.5, 0.5)
    node:setPosition(0, 0)

    local body = cc.PhysicsBody:create()
    local segment = cc.PhysicsShapeEdgeSegment:create(cc.p(0, -kOuterBottomSize+1), cc.p(self.visibleSize.width, -kOuterBottomSize+1), self.material)
    body:addShape(segment)
    body:setDynamic(false)
    body:setCategoryBitmask(kCategoryBottom)
    body:setContactTestBitmask(kCategoryBall)

    -- ノードに物体を設定
    node:setPhysicsBody(body) 

    return node
end

ここまでで、実行してみると外壁と左下にボールとパッドが表示されるはずです。(デバッグ用に物体の形状を表示するようにしています。)
スクリーンショット 2014-09-23 17.31.42.png

ブロックの作成と削除

次にブロックを作成していきます。ブロックはレイヤーを作成し、その上に複数のブロックを作成します。
まだ使用しませんが、ゲームオーバやゲームクリア時に再スタートするためのブロック削除用のメソッドも作っています。

GameScene.lua
-- ブロックレイヤーの作成
function GameScene:createBlockLayer()
    local layer = cc.Layer:create()
    layer:setName("blockLayer")

    self.blockCount = 0

    for i = 0, kBlockVerticalCount-1 do
        for j = 0, kBlockHorizontalCount-1 do
            local block = cc.Sprite:createWithTexture(self.blockTexture, cc.rect(0, i*kBlockHeight, kBlockWidth, kBlockHeight))
            block:setPosition(j * kBlockWidth + block:getContentSize().width/2, self.visibleSize.height - kTopMargin - i * kBlockHeight - block:getContentSize().height/2)
            layer:addChild(block)

            local physicsBody = cc.PhysicsBody:createBox(block:getContentSize(), self.material)
            physicsBody:setDynamic(false)
            physicsBody:setCategoryBitmask(kCategoryBlock)
            physicsBody:setContactTestBitmask(kCategoryBall)

            block:setPhysicsBody(physicsBody)

            self.blockCount = self.blockCount + 1
        end
    end

    return layer
end

-- ブロックレイヤーの削除
function GameScene:removeBlockLayer()
    local oldLayer = self.layer:getChildByName("blockLayer")
    if oldLayer ~= nil then 
        oldLayer:removeFromParent()
    end
end

ゲーム開始の準備

createBlockLayerはどこからもまだ呼ばれていませんので、まだブロックは表示されません。

そこで、ゲーム開始準備用のメソッドreadyToStartを記述して、その中からブロック作成等のメソッドを呼び出します。

readyToStartメソッドは、ゲーム起動時はゲームオーバーからの再開、ゲームクリアからの再開時に呼ばれます

GameScene.lua
-- ゲーム開始準備
function GameScene:readyToStart()
    self.gamePhase = GamePhase.ReadyToStart
    self:removeBlockLayer()
    self.layer:addChild(self:createBlockLayer())
    self.label:setString("Tap to Start")
    self.label:setVisible(true)
    self.paddle:setPosition(self.visibleSize.width / 2, kBottomMargin)
    self.ball:setPosition(self.visibleSize.width / 2, kBottomMargin + self.paddle:getContentSize().height/2 + self.ball:getContentSize().height/2) 
end

このメソッドでは、まず現在のゲーム進行状態(self.gamePhase)をReadyToStarに設定して、古いブロックを削除し、ブロックを新しく作成して画面に表示します。
次に、「Tap to start」の表示、パドルとボールの初期位置の設定を行っています。

ここまで実行すると画面にブロックが表示されたはずです。
スクリーンショット 2014-09-23 17.33.06.png

画面に「Tap to start」と表示されますが、タップしても何も反応はありません。
次にタッチイベントを記述していきます。

タッチイベント処理を実装する

タッチイベント処理を実装していきます。タッチイベントでは、ゲーム開始前のタッチ待ち、ゲームプレイ中、ゲームオーバー時と処理を分ける必要があります。

ゲームプレイ中のドラッグではパドルを移動する処理を行ってます。

GameScene.lua
-- タッチイベント処理
function GameScene:touchEvent()
    local previousLocation = nil
    -- タッチ開始
    local function onTouchBegan(touch, event)
        previousLocation = touch:getLocation()

        if self.gamePhase == GamePhase.ReadyToStart then
            self:gameStart()
            return false
        elseif self.gamePhase == GamePhase.GameOver or self.gamePhase == GamePhase.GameClear then
            self:readyToStart()
            return false
        end

        return true
    end

    -- タッチ移動
    local function onTouchMoved(touch, event)
        if self.gamePhase == GamePhase.Playing then
            local location = touch:getLocation()
            self.paddle:setPosition(self.paddle:getPositionX() + location.x - previousLocation.x, self.paddle:getPositionY())
            previousLocation = location
        end
    end

    local listener = cc.EventListenerTouchOneByOne:create()
    listener:registerScriptHandler(onTouchBegan, cc.Handler.EVENT_TOUCH_BEGAN )
    listener:registerScriptHandler(onTouchMoved, cc.Handler.EVENT_TOUCH_MOVED )
    local eventDispatcher = self:getEventDispatcher():addEventListenerWithSceneGraphPriority(listener, self)
end

ゲーム開始処理を記述する

画面をタップするとgameStartメソッドが呼ばれます。
gameStartメソッドでは、ラベルを消し、ボールの初速を決めてボールを打ち出します。

GameScene.lua
-- ゲーム開始
function GameScene:gameStart()
    self.gamePhase = GamePhase.Playing
    self.label:setVisible(false)
    local velocity = cc.pMul(cc.pNormalize(cc.p(1, 1)), kBallSpeed)
    self.ball:getPhysicsBody():setVelocity(velocity)
end

ここまで実行すると、画面タップでボールが画面上を飛び回りますが、ブロックは壊れませんし、ゲームオーバーにもなりません。
スクリーンショット 2014-09-23 17.48.06.png

また、なぜかボールの速度が低下していきます。(この理由についてはよくわかりませんが、とりあえず衝突処理中に速度低下防止の処理を入れておきます。原因がわかりましたらコードを修正するかもしれません。)

衝突処理を実装する

衝突処理を実装していきます。
どの物体が衝突したかは、CategoryBitmaskで判別しています。
まず、ボールとブロックが衝突した時、画面からブロックを削除します。
ボールと底面が衝突した時はゲームオーバーとします。

また、ボールの速度が減衰しないように、速度を一定にする処理を加えています。

GameScene.lua
-- 衝突時に呼ばれるイベント処理

function GameScene:contactTest()

    local function onContactPostSolve(contact)
        local a = contact:getShapeA():getBody()
        local b = contact:getShapeB():getBody()
        if a:getCategoryBitmask() > b:getCategoryBitmask() then a, b = b, a end

        -- ボールとブロックが衝突した時
        if b:getCategoryBitmask() == kCategoryBlock then
            b:getNode():removeFromParent()
            self.blockCount = self.blockCount - 1
            if self.blockCount == 0 then
                self:gameClear()
            end
        -- ボールと底面が衝突した時
        elseif b:getCategoryBitmask() == kCategoryBottom then
            self:gameOver()
        end

        -- ボール速度の減衰防止
        if a:getCategoryBitmask() == kCategoryBall and self.gamePhase == GamePhase.Playing then
            local velocity = a:getVelocity()
            if kBallSpeed * kBallSpeed > cc.pLengthSQ(velocity) then
                velocity = cc.pNormalize(velocity)
                velocity = cc.p(velocity.x * kBallSpeed, velocity.y * kBallSpeed)
                a:setVelocity(velocity)
            end
        end

        return true
    end

    -- 衝突時に指定した関数を呼び出すようにする
    local contactListener = cc.EventListenerPhysicsContact:create()
    contactListener:registerScriptHandler(onContactPostSolve, cc.Handler.EVENT_PHYSICS_CONTACT_POSTSOLVE)
    local eventDispatcher = self:getEventDispatcher():addEventListenerWithSceneGraphPriority(contactListener, self)
end

ゲームオーバーとゲームクリア処理を記述する

ボールが落下した時、ブロックがすべてなくなった時に、それぞれgameOverメソッド、gameClearメソッドが呼ばれますので、その処理を記述します。

GameScene.lua
-- ゲームオーバー
function GameScene:gameOver()
    self.gamePhase = GamePhase.GameOver
    self.label:setString("Game Over")
    self.label:setVisible(true)
    self.ball:getPhysicsBody():setVelocity(cc.p(0,0))
end

-- ゲームクリアー
function GameScene:gameClear()
    self.gamePhase = GamePhase.GameClear
    self.label:setString("Congratulations !!")
    self.label:setVisible(true)
    self.ball:getPhysicsBody():setVelocity(cc.p(0,0))
end

これですべてのコードの作成が終わりました。ここまでで実行するとパドルでボールを打ち返し、ブロックがすべてなくなるとゲームクリアとなります。
最小のコードで作ったのですが、ゲームっぽくなったのではないでしょうか?
スクリーンショット 2014-09-23 17.59.23.png

今後の課題

とりあえずブロック崩しっぽいものを作成しましたが、面白いゲームにするにはまだまだ課題があります。たとえば以下の様な課題がまだ残っています。

  • 1回でゲームオーバにならないようにライフ制にする
  • 音楽や効果音をつける
  • ブロック破壊時やボール落下時にパーティクル等で画面効果をつける
  • 面数制にして、ブロックの配置を多彩にする
  • タイトル画面やゲーム説明画面を用意する
  • ブロックや背景画像を凝ったものにする
  • 時間経過とともにボールの速度を上げる
  • ボールの角度が水平に近くなるとなかなか落ちてこないので、それに対処する
  • アイテム獲得などでパドルの大きさに変化をつけたり、ゲーム自体に変化をつける

ゲームとして完成させる道のりは長いですね。
これ以降は各自で面白いゲームに仕上げてみてください。

長かったですが、ここまで読んでいただき、本当にありがとうございました。

ソースコード

完成したゲームのソースコードはこちらからダウンロードできます。
github.com/senchan-office/Breakout