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言語)を参考に、音をつけてみてください。
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ディスプレイのように高解像度で画像が表示されます。
cc.Director:getInstance():getOpenGLView():setDesignResolutionSize(320, 480, cc.ResolutionPolicy.SHOW_ALL) -- (480, 320, 0)から修正
cc.Director:getInstance():setContentScaleFactor(2.0) -- 表示比率
ここでは音楽は鳴らしませんので以下の行をコメントアウトします。
--gameScene:playBgMusic()
これで、main.luaの修正は終わりです。(Githubにあるファイルはこれより若干の変更を加えています。)
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です。
ここまでで準備は終わりです。
いよいよゲームのコードを記述していきます。
ゲームのコードを記述する
コードとしてはそれほど複雑ではありません。
- 定数やコンストラクタcreateメソッド等の準備
- レイヤとそこに配置するボールなどのオブジェクトの作成
- ゲーム開始、ゲームオーバーなど状態ごとの処理
- タッチイベント、衝突イベントなどイベント処理
に大きく分かれます。
定数を記述する
コードの先頭にゲーム内で使用する定数を記述していきますが、Luaではconstなどが利用できないようなので、定数もどきにして記述していきます。
中略・・・
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などをいきなり使っても動作してしまいます。
あくまで、わかりやすさのために、「この後こんな変数を使いますよ」という意味で書いています。
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メソッドに処理を記述していきます。
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()でゲーム開始準備に入ります。
メインのレイヤーを作成する
メインのレイヤーを作成し、その上にパドル、ボール、ゲームオーバーなどの文字を表示するラベル、画面サイズの壁を配置していきます。
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言語を参照してください。)
-- パドルの作成
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を使っていて、小さくて速い物体は線をすり抜けることがあるようです。対処方法がわかりませんでしたので、壁に厚みをつけて対処しています。)
-- 画面の外周に壁を作る
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
ミス判定用の線を作る
ボールが画面下に落下した時にゲームオーバーとします。ゲームオーバの判定用に画面下に線を作り、ボールとこの線が接触したらゲームオーバーと判定しています。
-- 底面にミス判定用の線を作る
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
ここまでで、実行してみると外壁と左下にボールとパッドが表示されるはずです。(デバッグ用に物体の形状を表示するようにしています。)
ブロックの作成と削除
次にブロックを作成していきます。ブロックはレイヤーを作成し、その上に複数のブロックを作成します。
まだ使用しませんが、ゲームオーバやゲームクリア時に再スタートするためのブロック削除用のメソッドも作っています。
-- ブロックレイヤーの作成
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メソッドは、ゲーム起動時はゲームオーバーからの再開、ゲームクリアからの再開時に呼ばれます
-- ゲーム開始準備
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」の表示、パドルとボールの初期位置の設定を行っています。
画面に「Tap to start」と表示されますが、タップしても何も反応はありません。
次にタッチイベントを記述していきます。
タッチイベント処理を実装する
タッチイベント処理を実装していきます。タッチイベントでは、ゲーム開始前のタッチ待ち、ゲームプレイ中、ゲームオーバー時と処理を分ける必要があります。
ゲームプレイ中のドラッグではパドルを移動する処理を行ってます。
-- タッチイベント処理
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メソッドでは、ラベルを消し、ボールの初速を決めてボールを打ち出します。
-- ゲーム開始
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
ここまで実行すると、画面タップでボールが画面上を飛び回りますが、ブロックは壊れませんし、ゲームオーバーにもなりません。
また、なぜかボールの速度が低下していきます。(この理由についてはよくわかりませんが、とりあえず衝突処理中に速度低下防止の処理を入れておきます。原因がわかりましたらコードを修正するかもしれません。)
衝突処理を実装する
衝突処理を実装していきます。
どの物体が衝突したかは、CategoryBitmaskで判別しています。
まず、ボールとブロックが衝突した時、画面からブロックを削除します。
ボールと底面が衝突した時はゲームオーバーとします。
また、ボールの速度が減衰しないように、速度を一定にする処理を加えています。
-- 衝突時に呼ばれるイベント処理
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メソッドが呼ばれますので、その処理を記述します。
-- ゲームオーバー
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
これですべてのコードの作成が終わりました。ここまでで実行するとパドルでボールを打ち返し、ブロックがすべてなくなるとゲームクリアとなります。
最小のコードで作ったのですが、ゲームっぽくなったのではないでしょうか?
今後の課題
とりあえずブロック崩しっぽいものを作成しましたが、面白いゲームにするにはまだまだ課題があります。たとえば以下の様な課題がまだ残っています。
- 1回でゲームオーバにならないようにライフ制にする
- 音楽や効果音をつける
- ブロック破壊時やボール落下時にパーティクル等で画面効果をつける
- 面数制にして、ブロックの配置を多彩にする
- タイトル画面やゲーム説明画面を用意する
- ブロックや背景画像を凝ったものにする
- 時間経過とともにボールの速度を上げる
- ボールの角度が水平に近くなるとなかなか落ちてこないので、それに対処する
- アイテム獲得などでパドルの大きさに変化をつけたり、ゲーム自体に変化をつける
ゲームとして完成させる道のりは長いですね。
これ以降は各自で面白いゲームに仕上げてみてください。
長かったですが、ここまで読んでいただき、本当にありがとうございました。
ソースコード
完成したゲームのソースコードはこちらからダウンロードできます。
github.com/senchan-office/Breakout