TOP > プログラム関連 > Lua > LÖVE(Love2D) > LÖVE(Love2D)でのシーン遷移のサンプル

◆◆ LÖVE(Love2D)でのシーン遷移のサンプル ◆◆

ゲームと言えばシーン遷移!
「タイトル画面 → キャラクター選択 → ゲーム本番 → リザルト表示」などなど。
LÖVEでこういうシーンの切り替えを実装する方法のサンプルを愛する皆さんとシェアいたしますよ。

サンプルとして、こちらを使います。 (モデル:東北ずん子さん →公式サイト

それぞれ「モチつきパート → ずんだ錬成パート → 出来上がり(リザルト)表示」などと想像力を発揮して読み替えるべしです。
掲載しているソースコードは好きに使ってくれて構いません。天才プログラマーの私が書いたものですので鵜呑みにすべしです。

目次

この記事の対象者
↑目次へ↑

Lua自体がどんなものか簡単に確認したいお友達はこちらへ → Luaの超駆け足での解説(チートシート)
LÖVEのお勉強をしたいお友達はこちらへ(外部サイト様) → LÖVE(Love2D) プログラミング入門
一切衆生への慈愛の心を持ちたいお友達はこちらへ → 学園棋神伝マインドル

この記事のポイント
↑目次へ↑
「シーン」ごとに処理を分けるフレームワークを確保

中身をどうするかはさておき、ゲームのパートごとに処理を分けてソースを書いていけるような「枠組み」を用意しておくと、 開発の途中で遭難するリスクを減らせること請け合いです。

画面切り替え時のエフェクトは後でどうにでも変更できる

冒頭の動画ではサンプルとしてエフェクトをイロイロ用意してみましたが、実際には1つのゲームで1種類あれば充分なケースが多いような気がします。 ここでの要点としては、シーン遷移の枠組みさえ作っておけば、エフェクト自体は後でいくらでも差し替え可能ですよ、ということです (エフェクトの内容自体の解説はあんまりしません。ソースをご参照いただければと思います)

シーン管理クラスと個別のシーンクラス
↑目次へ↑

まずは遷移のエフェクトなしで、シーンの切り替えの仕組みだけを考えます。

シーンマネージャー

考え方。 個別のシーンの上位に、統括役のクラスを挟みます(これをシーンマネージャーとします)。 で、その下に、個別のシーンクラスがリストとしてぶら下がる形にする。

個別のシーンのうちのどれか1つだけを「現在のシーン」ということにして、 そのシーンの処理だけを毎フレーム実行するようにします。 で、必要に応じて、「現在のシーン」を切り替えていけばよさそうですね。

インターフェイスの要点

LÖVE自体が既にフレームワークであり、main.lua の「更新(update)」と「描画(draw)」が毎フレーム呼ばれるようになっています。 というわけで、それに合わせてシーンマネージャーにも「update」「draw」というインターフェイスを持たせることとします。

同様に、各シーンの側にも「update」「draw」というインターフェイスを持たせ、 シーンマネージャーから「現在のシーン」へと、それらのインターフェイスを通して処理を受け渡していくようにします。

シーンを切り替えるためのインターフェイスをシーンマネージャー側に持たせておきます。 さらに、個別のシーン側にも、「シーン開始」「シーン終了」の通知を受け取るインターフェイスを用意しておき、 つつがなく遷移を行えるようにしておきます。 この中に具体的に何の処理を書くかは実際のシーンの内容次第です。必要なければ空白にしておきます。


ソースに落とし込んでいく

では、以上の考え方にもとづいて、実際のソースへと落とし込んでいきましょう。 まずは main.lua から。

LÖVEのテンプレ通り、load と update と draw を実装します。 load で シーンマネージャーを用意。 update と draw のそれぞれで、シーンマネージャーの update と draw を呼びます。
なお、この段階ではまだ SceneManager.lua は作ってませんが、あることにします。

加えて、本サンプルではマウスのクリックでシーンの切り替えをすることにしますので、 マウス入力を受け付ける mousereleased も用意しておきます。 中身はと言えば、これまたシーンマネージャーに丸投げするだけです。

ファイル名:main.lua
require( "sceneManager" )
-- 読み込み function love.load () sceneManager = SceneManager.new() sceneManager:init() end -- 更新 function love.update (dt) sceneManager:update(dt) end -- 描画 function love.draw () sceneManager:draw() end
-- マウス入力受付 function love.mousereleased ( x, y, buttonNum, istouch ) sceneManager:mousereleased ( x, y, buttonNum ) end


ではさて、さきほど後回しにしたシーンマネージャークラスを用意します。
main.lua の load、update、draw に対応するような形で init、update、draw を持たせます。

init で個別のシーンのクラスをインスタンス生成して、リストとして確保。

update と draw で「現在のシーン」の更新と描画を呼ぶようにします。 ここのところは main.lua から順に更新・描画処理を受け渡しているだけですね。

あ、言い忘れてましたが、マウス入力を受け付ける処理も、main.luaから受け取る形で同様に用意しておきます。 その中味も、これまた「現在のシーン」に受け渡すだけです。

さて、シーンマネージャーにはシーン切り替えの機能を持たせるのでした。 changeScene で「次のシーン名」を受け取って切り替え処理を行います。 その時点での「現在のシーン」の終了処理を呼んで、「次のシーン」の開始処理を呼び、「現在のシーン」と差し替える、という流れです。

ちなみにアプリケーションの開始時(init)、「最初のシーン」の開始処理を直書きで呼んでますが、このへんは適当に。

ファイル名:sceneManager.lua
require( "scene1" ) require( "scene2" ) require( "scene3" ) SceneManager = {} SceneManager.new = function () local this = {}
--========================================================= -- 初期設定 --========================================================= this.init = function ( self ) self.sceneList = { scene1 = Scene1.new(), scene2 = Scene2.new(), scene3 = Scene3.new(), } -- 最初のシーン self.currentScene = "scene1" self.sceneList[self.currentScene]:enter() end
--========================================================= -- 更新 --========================================================= this.update = function(self, dt) self.sceneList [ self.currentScene ]:update(dt) end --========================================================= -- 描画 --========================================================= this.draw = function(self) love.graphics.clear(1,1,1,1) love.graphics.setColor(1,1,1,1) self.sceneList [ self.currentScene ]:draw() end
--========================================================= -- マウス入力受付 --========================================================= this.mousereleased = function ( self, x, y, buttonNum ) if ( self.sceneList[self.currentScene].mousereleased ) then self.sceneList[self.currentScene]:mousereleased ( x, y, buttonNum ) end end
--========================================================= -- 次のシーンへ遷移 --========================================================= this.changeScene = function ( self, nextSceneName ) self.sceneList [ self.currentScene ]:leave() self.sceneList [ nextSceneName ]:enter() self.currentScene = nextSceneName end
return this end


あとは個別のシーンクラスです。 ここでは例として「モチつきシーン」を取り上げますが、他のシーンもパターンは同じです。

序盤の箇所は、モチつきアニメーション用の画像を読み込んだりしています。

これまで同様、処理を受け取るためのインターフェイスとして、更新(update)、描画(draw)と、ついでにマウス入力受付(mousereleased)を用意します。

シーンの切り替えは、「左クリックを押したとき」に実行するようにしています。

それからシーンの開始と終了の通知を受け取るインターフェイスも用意。 このサンプルでは中味は実質的に空になってますが、必要に応じて入れていくということですね。

あとは実際に「モチつきシーン」になるように、各部に処理をゴリゴリと書き込みます。

「ずんだ錬成」「出来上がり」のシーンもパターンは同様です。

ファイル名:scene1.lua
Scene1 = {} Scene1.new = function () local this = {}
this.id = "モチつきシーン" this.backGround = love.graphics.newImage ( "background.png" ) this.images = { love.graphics.newImage ( "mochi01_600_600.png" ), love.graphics.newImage ( "mochi02_600_600.png" ), love.graphics.newImage ( "mochi03_600_600.png" ), love.graphics.newImage ( "mochi04_600_600.png" ), love.graphics.newImage ( "mochi05_600_600.png" ) } this.imageIndex = 1 this.imageFramcount = 0
--========================================================= -- 更新 --========================================================= this.update = function(self, dt) self.imageFramcount = self.imageFramcount + 1 if ( self.imageFramcount > 4 ) then self.imageFramcount = 0 self.imageIndex = self.imageIndex + 1 if ( self.imageIndex > #self.images ) then self.imageIndex = 1 end end end --========================================================= -- 描画 --========================================================= this.draw = function(self) love.graphics.draw ( self.backGround, 80, 60 ) love.graphics.draw ( self.images[self.imageIndex], 400, 300, 0, 1, 1, self.images[self.imageIndex]:getWidth()/2, self.images[self.imageIndex]:getHeight()/2 ) end
--========================================================= -- マウスボタン受付 --========================================================= this.mousereleased = function ( self, x, y, buttonNum ) if ( buttonNum == 1 ) then sceneManager:changeScene("scene2") end end
--========================================================= -- シーン開始 --========================================================= this.enter = function(self) print("開始:"..self.id) end --========================================================= -- シーン終了 --========================================================= this.leave = function(self) print("終了:"..self.id) end
return this end

ここまでを組み合わせて実行すると、こんな動きになるハズです。やったね!

「モチつきシーン」以外のシーンも組み込んだ状態での全ソースコードはこちらに置いておきます。
LÖVEのシーン遷移サンプル(エフェクトなし)ソース.zip

実行には別途 LÖVE環境が必要です。
LÖVE公式サイト

アセット(東北ずん子さん)の入手元も改めてお伝えしておきます。
(モデル:東北ずん子さん →公式サイト

遷移エフェクト
↑目次へ↑

エフェクトは無くても機能的に問題があるわけではありませんが、それを言い出せば「無くてもいいけど、あると嬉しい」のがゲームというもの。 というわけで遷移時にエフェクトを入れることができるよう、ちょこっとだけ拡張していきまます。

フェードクラス

考え方。 フェード「アウト」「イン」を担当する専用のクラスを用意しておいて、シーンマネージャーのシーン切り替え処理の際に呼び出すこととします。 処理を分離しておくのがミソで、こうしておくことでフェードのエフェクト自体は後でいくらでも作り込んだり差し替えたりできるようになります。

インターフェイスの要点

まず、どういうエフェクトにするにせよ共通した処理内容としてはこんな感じです:

・「開始」するとフェードアウトを開始
・目一杯フェードアウトしたら、シーン遷移を実行
・フェードイン処理に移行
・フェードインが完了したら、役割終了

ポイントとしては、いつ実際にシーン遷移を実行すべきかは、フェードクラス側(だけ)が知っている、ということです。 ただし、その処理の中味はシーンマネージャーさんの領分であり、フェード担当氏にとっては、あずかり知らぬことです。

というわけでインターフェイス的には3点:

・外部から呼べる「開始」メソッドを持つこと
・どんなシーン遷移処理すればいいのかを教えておけること
・フェードインが完了したことが外部から確認できること

あとは他のオブジェクトと同様、update と draw を持たせます。


ソースへの落とし込み

ここでは例として「小さな黒い四角形で画面を埋めていくやつ」を掲載します。 エフェクトの内容はさておき、フェード処理担当クラスとしての要点は他のエフェクトの場合も同じです。

冒頭は本エフェクト特有のデータを用意しているところ。

「開始」メソッドでデータの中味を作ります。 この実装だと「開始」のたびにグリッドを並び替えているので無駄と言えば無駄かもしれません。

「更新」の中味は「フェードアウト」と「フェードイン」に分かれています。 処理の実体は「フェードの度合いを進めて → 戻す」です。度合いに応じてどう描画するかはdraw()側で。

「アウト」の終了時、実際のシーン遷移処理を起動します(シーンマネージャーから受け取った関数オブジェクト)。

フェード処理を入れる以上、実際にシーン遷移するタイミングを知っているのはフェード処理クラス、という考え方です。

「描画」処理の内容は、その時点でのフェードの度合いに応じて、 「グリッドを何個、黒塗り描画するか?」ということです。
事前にグリッドの配列をランダムにシャッフルしてありますので、狙い通りのエフェクトになります。

ファイル名:fade_grids.lua
Fade_grids = {} Fade_grids.new = function () local this = {}
-- フェードの進行度(0.0:フェードなし 1.0:画面真っ暗) this.fadeRate = 0.0 -- 処理フェイズ:フェード「in」か「out」か。 this.phase = "" -- ウィンドウのサイズを確認して、 local windowWidth, windowHeight = love.window.getMode() -- 特定のサイズのグリッドで画面を敷き詰めていく this.gridSize = 25 this.grids = {} -- 画面全体を敷き詰めるようにグリッドを作成 for y = 0, windowHeight, this.gridSize do for x = 0, windowWidth, this.gridSize do -- グリッドの実体は「座標」 this.grids [ #this.grids + 1 ] = { x = x, y = y } end end
--========================================================= -- 開始 --========================================================= this.start = function(self) -- 乱数初期化 math.randomseed(os.time()) self.fadeRate = 0.0 self.phase = "out" -- グリッドをシャッフル:ランダムに埋まっていく感じになる。 for i = #self.grids, 1, -1 do local j = math.random(1,i) local temp = self.grids[i] self.grids[i] = self.grids[j] self.grids[j] = temp end end
--========================================================= -- 更新 --========================================================= this.update = function(self) local pace = 0.03 -- フェードアウト if ( self.phase == "out" ) then -- フェードの度合いを「進める」 self.fadeRate = self.fadeRate + pace -- フェードアウト終了判定 if ( self.fadeRate >= 1.0 ) then -- ハミ出しを一応抑えて self.fadeRate = 1.0 -- 処理フェイズを「イン」に移行 self.phase = "in" -- シーン遷移を実行 -- 中味はシーンマネージャーから事前に受け取る self:changeScene() end -- フェードイン elseif ( self.phase == "in" ) then -- フェードの度合いを「戻す」 self.fadeRate = self.fadeRate - pace -- フェードイン終了判定 if ( self.fadeRate <= 0.0 ) then self.fadeRate = 0.0 self.phase = "" end end end
--========================================================= -- 描画 --========================================================= this.draw = function(self) -- 現時点の描画色を保持しておく(後で戻す) local r,g,b,a = love.graphics.getColor() -- 描画色を「黒塗り」にする love.graphics.setColor(0,0,0,1) -- フェード進行度合い(全グリッド中、いくつまで黒塗りにするか?) local progress = #self.grids * self.fadeRate -- 全グリッドでループして、 for i = 1, #self.grids, 1 do -- 現在のフェード進行度に応じた個数まで、黒塗りする if ( i <= progress ) then local x = self.grids[i].x local y = self.grids[i].y love.graphics.rectangle ( "fill", x, y, self.gridSize, self.gridSize ) end end -- 描画色を戻しておく love.graphics.setColor(r,g,b,a) end
return this end

シーンマネージャー側への組み込み

エフェクトが無いバージョンでは、「次のシーンへ」の処理はその場で即時実行でしたが、 エフェクトがあるバージョンでは、フェード用クラスをセッティングします。 シーン遷移処理はフェードクラス側に任せることにして、シーンマネージャーではフェードクラスの更新と描画を淡々と実行し続けるのみ、となります。

ソースと動作サンプル

冒頭のサンプルはサンプルなので、エフェクト自体を複数お見せできるような実装にしています。 内部的に複数のフェードクラスのオブジェクトを持ち、ユーザーの操作で切り替えられるようにしているのですが、 エフェクト自体が1つでよければ、そこまでする必要はありませんね。

というわけで、ひとまずここで、実際のソースに落とし込むこととします。
以下、エフェクト無し版との差分箇所をハイライトしてお送りします(桃色パート)
特に重要なのは「changeScene」の中味です(黄色パート)

ファイル名:sceneManager.lua
require( "scene1" ) require( "scene2" ) require( "scene3" )
require( "fade_grids" )
SceneManager = {} SceneManager.new = function () local this = {} --========================================================= -- 初期設定 --========================================================= this.init = function ( self ) self.sceneList = { scene1 = Scene1.new(), scene2 = Scene2.new(), scene3 = Scene3.new(), } -- 最初のシーン self.currentScene = "scene1" self.sceneList[self.currentScene]:enter()
self.nextScene = "" self.fadeProcessor = Fade_grids.new() self.fadeObj = nil
end --========================================================= -- 更新 --========================================================= this.update = function(self, dt)
if ( self.nextScene == "" ) then -- フェードアウト中は更新停止とする
self.sceneList [ self.currentScene ] : update(dt)
end
-- フェードがセットされていればフェード処理を更新 if ( self.fadeObj ) then self.fadeObj:update() -- フェード処理の終了判定 if ( self.fadeObj.phase == "" ) then self.fadeObj = nil end end
end --========================================================= -- 描画 --========================================================= this.draw = function(self) love.graphics.clear(1,1,1,1) love.graphics.setColor(1,1,1,1) self.sceneList [ self.currentScene ] : draw()
-- フェードがセットされていればフェードを描画 if ( self.fadeObj ) then self.fadeObj:draw() end
end --========================================================= -- マウス入力受付 --========================================================= this.mousereleased = function ( self, x, y, button ) if ( self.sceneList[self.currentScene].mousereleased ) then self.sceneList[self.currentScene]:mousereleased ( x, y, button ) end end --========================================================= -- 次のシーンへ遷移 --========================================================= this.changeScene = function ( self, nextSceneName )
self.nextScene = nextSceneName self.fadeObj = self.fadeProcessor -- フェードクラス側で然るべきタイミングで呼んでもらうためのシーン遷移処理を設定 self.fadeObj.changeScene = function () self.sceneList [ self.currentScene ]:leave() self.sceneList [ self.nextScene ]:enter() self.currentScene = self.nextScene self.nextScene = "" end self.fadeObj:start()
end return this end

なお、今回のサンプルではフェードアウトの時点で「現在のシーン」の更新を停止しています。 一方、フェードインが開始した時点で、更新が再開されるようにしています。 このへんは操作感のデザインに影響するところですので、適宜ヨシナに取り計らうとよいのではないかと思います。

この状態のソースはこちら。
LÖVEのシーン遷移サンプル(エフェクト1つ)ソース.zip

参考までに、複数の遷移エフェクトを切り替えられる版(記事冒頭のサンプルのやつ)のソースも置いておきます(切り替えは右クリック)。
LÖVEのシーン遷移サンプル(エフェクト複数)ソース.zip

LÖVEと東北ずん子さんの公式サイトも何度でも載せておきます。
LÖVE公式サイト
東北ずん子さん公式サイト

補足1:Luaでのクラスの雛形
↑目次へ↑

何の説明もなしに突っ走ってきましたが、 Luaでの「クラス」の書き方について軽く補足しておきます。

「テーブル」を利用して夢を叶える

Lua自体には Java や C++ のような「クラスッッ!!」というそのものスバリの機能はなく、 「テーブル」という配列と連想配列のオイシイところを合わせた感じの機能を利用して同等の機能をゴリゴリと実現します。

サンプルソース

ClassSample = {} ClassSample.new = function () local this = {} this.property1 = 0 this.property2 = 0 this.property3 = 0 this.method1 = function ( self ) self.property1 = 10 end this.method2 = function ( self, value ) self.property2 = value end this.method3 = function ( self, value1, value2 ) self.property3 = value1 + value2 end return this end

テーブルには関数を代入することも可能です。
new というコンストラクタっぽい名前で関数を代入というか定義し、 「ひとそろいのプロパティとメソッド」を持ったテーブルを戻り値として返却しています。 これで「インスタンス生成」のようなことを実現しています。

各メソッドの第一引数の self というのは、呼び出し元の自己自身を参照するための変数です。

使用例はこちら。
メソッド呼び出しの演算子にはコロン「:」を使います。 これは Lua のシンタックスシュガーで、 第一引数に呼び出し元の自己自身(obj)を自動的に入れてくれます。

obj = ClassSample.new() obj.property1 = 100 obj:method1()


補足2:事例紹介『つながあるふるエル三原色』
↑目次へ↑

今回の記事と(ほぼ)同じ手法を使って作ったゲームです。
愛する皆さんのためにソースコードも公開してます。
(ソースコードは当サイトのゲームページにて → http://www.site-a.info/game.html






    TOP > プログラム関連 > Lua > LÖVE(Love2D) > LÖVE(Love2D)でのシーン遷移のサンプル