敵人 (Enemy)
遊戲中除了主角,最重要的角色就是敵人了。太弱的敵人會讓關卡變得無聊,太強又會玩不下去,所以當你在設計敵人的時候,請務必根據遊戲的平衡性來調整敵人強度。
以上的例子中,我們都使用 EnemyPlane.lua 來建立敵人物件。如果想要自訂敵人,讓它有更帥氣的外表、更有威脅性的攻擊手段,應該要怎麼做?
新增檔案
首先你必須新增描述敵人的物件檔案:MyEnemy.lua (名字自訂),並在其中添加程式碼,繼承 Enemy.lua (第 6 行)。檔案的位置根據需求可以放在不同的地方,這邊我們將他放在:levels/myLevel 底下。檔案的位置與名稱會影響之後引用的它路徑。
local Enemy = require("Enemy")
local Sprite = require("Sprite")
local MyEnemy = {}
MyEnemy.new = function(options)
local myEnemy = Enemy.new(options)
return myEnemy
end
return MyEnemy
建立零件
只是繼承 Enemy.lua 並不會產生圖像,它只是 Corona SDK 中的一個顯示群組(Display Group),讓你可以加入自訂的圖像。這裡我們使用本模板提供的圖像產生工具 Sprite.lua,產生飛船的零件,拼裝我們的敵人。Sprite.lua 是非常方便的工具,它可以大幅縮減 Corona SDK 預設的圖像產生流程。
要使用 Sprite.lua,我們得先知道資源的路徑,你可以從 https://gitlab.com/keviner2004/shooting-art/tree/master/default 將資源下載回來,再用 “對照” 的方式尋找資源。舉例來說:位於 /default/Ships/Parts/Cockpits/Bases/18 位置的圖檔對應的 Sprite 資源位置為 Ships/Parts/Cockpits/Bases/18。
Sprite.lua 會根據指定的資源位置在 Spritesheet 內尋找正確的圖像。所謂的 Spritesheet 是由一連串的圖像組合而成的,如下圖:
Spritesheet 的好處是可以減少圖像讀取的時間,並且縮小圖像佔用的記憶體空間。當你告知了 Sprite.lua 資源位置後,它便會根據該位置找到目標圖像在 Spritesheet 內對應的座標與長寬,最後從預載好的 Spritesheet 內將資源切割出來呈限給顯示端。上述這些複雜的步驟都已經透過 Corona SDK 與 Sprite.lua 處理了,也因此你才能像以下的例子一樣那麼簡單的顯示圖像。
接下來我們示範如何拼揍出以下的敵人:
先在程式碼內建立相關零件:
local part1 = Sprite.new("Ships/Parts/Cockpits/Bases/18")
local part2 = Sprite.new("Ships/Parts/Cockpits/Glass/25")
--left wing
local part3 = Sprite.new("Ships/Parts/Wings/68")
--right wing
local part4 = Sprite.new("Ships/Parts/Wings/68")
local part5 = Sprite.new("Ships/Parts/Engines/6")
local part6 = Sprite.new("Ships/Parts/Engines/6")
--left gun
local part7 = Sprite.new("Ships/Parts/Guns/8")
--right gun
local part8 = Sprite.new("Ships/Parts/Guns/8")
此範例使用到的資源路徑與圖像的對應如下表:
路徑 | 預覽 |
---|---|
Ships/Parts/Cockpits/Bases/18 | |
Ships/Parts/Cockpits/Glass/25 | |
Ships/Parts/Wings/68 | |
Ships/Parts/Engines/6 | |
Ships/Parts/Guns/8 |
加入零件
接著將新增好的零件加入新建立的敵人物件內,物件顯示的順序決定於加入的順序,先加入物件顯示順序較低,越後面加入的物件會顯示在越前面。
--insert to enemy
myEnemy:insert(part7)
myEnemy:insert(part8)
myEnemy:insert(part5)
myEnemy:insert(part6)
myEnemy:insert(part3)
myEnemy:insert(part4)
myEnemy:insert(part1)
myEnemy:insert(part2)
設定屬性
遊戲物件移動的時候,通常需要根據物件指向的位置旋轉,也因此我們會需要定義物件指向的方向 : myEnemy.dir。從上圖得知,這個物件指向的位置是 270 度,所以我們將 myEnemy.dir 設置為 270,指向下方 (這個值預設為 90,指向上方)。
--set properties
myEnemy.dir = 270
反轉圖像
為了節省系統資源,遊戲的實作上往往會利用相同的圖像去達成不同的效果,在這個例子裡,我們翻轉右方的機翼雨左方的槍管,來實作成左方的機翼與右方的槍管。
--flip
part3.xScale = -1
part8.xScale = -1
變數 | 翻轉前 | 翻轉前 |
---|---|---|
part3 | ||
part8 |
定位零件
當在實作遊戲的時候,會有多重解析度的議題:我們會希望具有高解析度螢幕的裝置,採用更清晰的圖源來顯示遊戲物件,例如 720x1080 的解析度會採用 [email protected],1080x1920 則會採用 [email protected]。
不同圖源的長寬像素是不同的,所以建議你不要直接指定像素來定位零件:part2.y = -10 ,而是用比例的方式:part2.y = part1.height/8。
以下的例子裡都會根據 part1 的長寬比例來定位零件,以解決多解析度的問題。
--position
part2.y = part1.height/8
part3.x = -part1.width/4*3
part3.y = -part1.height/4
part4.x = part1.width/4*3
part4.y = -part1.height/4
part5.x = -part1.width/2
part5.y = -part1.height/4
part6.x = part1.width/2
part6.y = -part1.height/4
part7.x = -part1.width/2
part7.y = part1.height/4
part8.x = part1.width/2
part8.y = part1.height/4
設定子彈
本模板提供一個非常簡單的方式讓你的敵人發射子彈,首先你先指定子彈來源與建構此子彈所需要的參數。
接著只需要使用 myEnemy:shoot 發射子彈即可。下面的例子使用 addTimer 達到每 1 秒發射 1 個子彈的效果。
注意該 myEnemy:addTimer() 會在 myEnemy 被回收時跟著被回收。這意味著當 myEnemy 透過 myEnemy:clear()被回收時,便不會繼續執行 shoot()。
--setup shoot
myEnemy:setDefaultBullet("bullets.Laser", {laserFrame = "Lasers/2"})
myEnemy:addTimer(1000,
function()
myEnemy:shoot({x = myEnemy.x + part1.width/2 , degree = myEnemy.dir, speed = 1000})
myEnemy:shoot({x = myEnemy.x - part1.width/2 , degree = myEnemy.dir, speed = 1000})
end
, -1)
開啟物理引擎
如果你不希望你敵人是無敵狀態,你必須開啟物理引擎的功能,讓它能和其他物件碰撞。
--enable physic
myEnemy:enablePhysics()
全部的程式碼如下:
--levels/myLevel/MyEnemy.lua
local Enemy = require("Enemy")
local Sprite = require("Sprite")
local MyEnemy = {}
MyEnemy.new = function(options)
local myEnemy = Enemy.new(options)
local part1 = Sprite.new("Ships/Parts/Cockpits/Bases/18")
local part2 = Sprite.new("Ships/Parts/Cockpits/Glass/25")
--left wing
local part3 = Sprite.new("Ships/Parts/Wings/68")
--right wing
local part4 = Sprite.new("Ships/Parts/Wings/68")
local part5 = Sprite.new("Ships/Parts/Engines/6")
local part6 = Sprite.new("Ships/Parts/Engines/6")
--left gun
local part7 = Sprite.new("Ships/Parts/Guns/8")
--right gun
local part8 = Sprite.new("Ships/Parts/Guns/8")
--set properties
myEnemy.dir = 270
--insert to enemy
myEnemy:insert(part7)
myEnemy:insert(part8)
myEnemy:insert(part5)
myEnemy:insert(part6)
myEnemy:insert(part3)
myEnemy:insert(part4)
myEnemy:insert(part1)
myEnemy:insert(part2)
--flip
part3.xScale = -1
part8.xScale = -1
--position
part2.y = part1.height/8
part3.x = -part1.width/4*3
part3.y = -part1.height/4
part4.x = part1.width/4*3
part4.y = -part1.height/4
part5.x = -part1.width/2
part5.y = -part1.height/4
part6.x = part1.width/2
part6.y = -part1.height/4
part7.x = -part1.width/2
part7.y = part1.height/4
part8.x = part1.width/2
part8.y = part1.height/4
--setup shoot
myEnemy:setDefaultBullet("bullets.Laser", {laserFrame = "Lasers/2"})
myEnemy:addTimer(1000,
function()
myEnemy:shoot({x = myEnemy.x + part1.width/2 , degree = myEnemy.dir, speed = 1000})
myEnemy:shoot({x = myEnemy.x - part1.width/2 , degree = myEnemy.dir, speed = 1000})
end
, -1)
--enable physic
myEnemy:enablePhysics()
return myEnemy
end
return MyEnemy
使用自訂的敵人物件
那麼你該如何使用自訂的敵人物件呢?很簡單,只需要先引用它 (第 2 行),接著透過它建立新物件(第 7 行) 就可以了。
local gameConfig = require("gameConfig")
local Sublevel = require("Sublevel")
local MyEnemy = require("levels.myLevel.MyEnemy")
local util = require("util")
local myLevel = Sublevel.new("9999999-011", "level name", "author name")
function myLevel:show(options)
local enemy = MyEnemy.new()
self:insert(enemy)
--place the enemy out of the screen
enemy.x = gameConfig.contentWidth/4
enemy.y = -100
--move the enemy from the top to bottom with speed 100 pixels/second
enemy:setScaleLinearVelocity( 0, 50 )
enemy:addItem("items.PowerUp", {level = 1})
--destroy the enemy properly
enemy:autoDestroyWhenInTheScreen()
self.enemy = enemy
end
function myLevel:isFinish()
--print("isFinish!??")
if util.isExists(self.enemy) then
return false
else
return true
end
end
return myLevel
掉落道具
以下為本模板預設的道具
資源路徑 | 功能 |
---|---|
items.PowerUp | 改變攻擊模式 |
items.ScoreUp | 增加分數 |
items.ShieldUp | 開啟護盾 |
items.SpeedUp | 增加射擊速度 |
如果你要設定敵人會掉落道具,你可以透過 addItem 新增它,第一個參數是道具的資源路徑,第二個參數則是初始化這個道具時會需要用到參數。
enemy:addItem("items.PowerUp", {level = 1})
魔王
建立一個魔王
魔王是敵人的一種,所以要新增魔王的第一步便是新增一個敵人的類別檔案,這裡我們沿用之前建立自訂敵人時的例子,並修改成一個魔王。
更多的血量
魔王通常擁有更多的血量,為了讓之後的 HP 條顯示正確的血量,我們除了透過 hp
來設定更多的血量外,也指定 maxHp
表示血量的最大值:
myEnemy.maxHp = 1000
myEnemy.hp = 1000
多個階段
一般來說,魔王會有多個階段,例如當血量低於某個數值,該魔王就會使用全新的攻擊手段,這個模板提供一個方便的階段管理器 PhaseManager
來實現這樣的功能,首先我們先引用 PhaseManager
,並使用 new
來新增一個實體:
local PhaseManager = require("PhaseManager")
--... some codes
local phaseManager = PhaseManager.new()
這個例子中,我們希望魔王有三個階段,第一個階段:魔王會發射一發子彈,第二個階段:魔王會發射兩發子彈,第三個階段,魔王會發射三發子彈以及追蹤導彈。其中第一個階段為初始階段,當魔王血量少於或等於 2/3 時會進入到第二階段,當魔王血量少於1/3 時則進入第三階段,血量歸0 時魔王則被消滅。
有了 phaseManager
的幫助,我們可以利用 phaseManager:registerPhase()
註冊每個階段,registerPhase()
有 4 個參數,分別為階段的名稱、該階段的執行動作、階段完成的條件以及階段完成時要被呼叫的函式。
phaseManager:registerPhase() 的語法
phaseManager:registerPhase(key, action, isFinish, onComplete)
其中 action
可以是表格,與事件呼叫的原理相同,當 action
為表格時,會尋找並執行表格內與 key
同名的函式,若 action
為函示時,則會直接呼叫該函式。
isFinish()
在最後會期待開發者回傳 true
或 false
,若回傳 true
,則代表該階段結束,並會進行下一個階段。
下一個階段是由 onComplete
的回傳值決定的,onComplete
期待開發者回傳下一個階段的名稱,舉例來說:如果 onComplete 最後回傳的是 stage2
,則下一個階段即是註冊為 stage2
的階段。
以下的程式碼註冊了三個接段:stage1
, stage2
以及 stage3
:
phaseManager:registerPhase(
"stage1",
myEnemy,
function()
return myEnemy.hp <= 666
end,
function()
return "stage2"
end
)
phaseManager:registerPhase(
"stage2",
myEnemy,
function()
return myEnemy.hp <= 333
end,
function()
return "stage3"
end
)
phaseManager:registerPhase(
"stage3",
myEnemy,
function()
return myEnemy.hp <= 0
end
)
當註冊完了階段,我們必須要決定何時去更新階段的資訊。當然我們可以每一幀去檢查,但這樣會消耗太多的系統資源,這裡我們採用比較聰明的方法:由於我們的階段結束條件都是血量有變化時發生的,所以我們可以在魔王的血量發生變化時去更新階段資訊就好。
這裡我們在魔王這個物件上註冊 health
事件,這樣一來我們會在魔王血量發生異動時接收到通知,收到通知的同時,我們使用 check()
檢查階段是否產生變化:
--enemy is hurt
myEnemy:addEventListener("health", function(event)
--logger:info(TAG, "HP Event: name:%s, phase:%s, crime:%s, damage:%s, hp:%s", event.name, event.phase, event.crime.name or "", event.damage, event.hp)
phaseManager:check()
end)
當然我們還必須要指定初始的階段,初始階段是魔王一開始所在的階段,會從這個階段前往到不同階段:
phaseManager:setCurrentPhase("stage1")
上述的例子中我們定義了魔王的三個階段與每個階段間的連結方式,現在該是去實作每個階段的 action
的時候了,當 PhaseManager
進入到另一個階段時便會執行該階段的 action
,由於這裡註冊階段時, action
是使用 myEnemy
物件,我們必須在 myEnemy
定義三個階段的 action
,並且函式名稱必須與當時註冊的階段名稱相同。
第一個階段 stage1
,我們一次只能發射一個子彈,所以我們先設定了魔王預設使用的子彈,並且使用一個 timer 去定期的發射一個子彈。
myEnemy:setDefaultBullet("bullets.Laser", {laserFrame = "Lasers/2"})
function myEnemy:stage1()
logger:info(TAG, "The boss is in stage1, shoot 1 bullet")
--setup shoot
self.preTimer = self:addTimer(1000,
function()
self:shoot({x = self.x , degree = self.dir, speed = 100 * gameConfig.scaleFactor})
end
, -1)
end
第二個階段 stage2
,與第一個階段類似,不同的是我們必須將前一階段的 timer 停止,否則會發射多餘的子彈,這也為什麼我們在第一階段要用 preTimer
將 timer id 記錄起來的原因,這樣我們才能在第二個階段使用 cancelTimer
取消該計時器。
function myEnemy:stage2()
logger:info(TAG, "The boss is in stage2, shoot 2 bullet")
self:cancelTimer(self.preTimer)
self.preTimer = self:addTimer(1000,
function()
self:shoot({x = self.x + self.width/4 , degree = self.dir, speed = 100 * gameConfig.scaleFactor})
self:shoot({x = self.x - self.width/4 , degree = self.dir, speed = 100 * gameConfig.scaleFactor})
end
, -1)
end
第三個階段就複雜了一點,因為我們要發射預設子彈外的子彈,所以我們使用參數 bulletClass
帶入這次要發射的子彈類別,和參數 bulletOptions
帶入該類別初始化需要的參數,並透過 onShoot
參數獲得子彈發射的事件,讓我們可以自己定義子彈發射的行為。除此之外,這裡還使用一個 counter
紀錄目前發射的子彈次數,達到每發射兩發一般子彈才發射一發追蹤導彈的效果。
function myEnemy:stage3()
local counter = 0
logger:info(TAG, "The boss is in stage3, shoot 3 bullet, and a homing missile")
self:cancelTimer(self.preTimer)
self.preTimer = self:addTimer(1000,
function()
counter = counter + 1
self:shoot({x = self.x + self.width/4 , degree = self.dir, speed = 100 * gameConfig.scaleFactor})
self:shoot({x = self.x , degree = self.dir, speed = 100 * gameConfig.scaleFactor})
if counter%2 == 1 then
self:shoot({
x = self.x , degree = self.dir, speed = 100 * gameConfig.scaleFactor,
bulletClass = "bullets.Missile",
bulletOptions = {},
onShoot = function(bullet)
bullet:setScaleLinearVelocity(0, 200)
bullet:rotateTo(270)
move.seek(bullet, self.players[1])
end
})
end
self:shoot({x = self.x - self.width/4 , degree = self.dir, speed = 100 * gameConfig.scaleFactor})
end
, -1)
end
全部程式碼如下:
--levels/myLevel/MyBoss.lua
local Enemy = require("Enemy")
local Sprite = require("Sprite")
local PhaseManager = require("PhaseManager")
local logger = require("logger")
local move = require("move")
local gameConfig = require("gameConfig")
local TAG = "MyBoss"
local MyEnemy = {}
MyEnemy.new = function(options)
local myEnemy = Enemy.new(options)
local part1 = Sprite.new("Ships/Parts/Cockpits/Bases/18")
local part2 = Sprite.new("Ships/Parts/Cockpits/Glass/25")
--left wing
local part3 = Sprite.new("Ships/Parts/Wings/68")
--right wing
local part4 = Sprite.new("Ships/Parts/Wings/68")
local part5 = Sprite.new("Ships/Parts/Engines/6")
local part6 = Sprite.new("Ships/Parts/Engines/6")
--left gun
local part7 = Sprite.new("Ships/Parts/Guns/8")
--right gun
local part8 = Sprite.new("Ships/Parts/Guns/8")
--set properties
myEnemy.dir = 270
myEnemy.maxHp = 1000
myEnemy.hp = 1000
myEnemy.players = options.players
--insert to enemy
myEnemy:insert(part7)
myEnemy:insert(part8)
myEnemy:insert(part5)
myEnemy:insert(part6)
myEnemy:insert(part3)
myEnemy:insert(part4)
myEnemy:insert(part1)
myEnemy:insert(part2)
--flip
part3.xScale = -1
part8.xScale = -1
--position
part2.y = part1.height/8
part3.x = -part1.width/4*3
part3.y = -part1.height/4
part4.x = part1.width/4*3
part4.y = -part1.height/4
part5.x = -part1.width/2
part5.y = -part1.height/4
part6.x = part1.width/2
part6.y = -part1.height/4
part7.x = -part1.width/2
part7.y = part1.height/4
part8.x = part1.width/2
part8.y = part1.height/4
--enable physic
myEnemy:enablePhysics()
local phaseManager = PhaseManager.new()
--[[
phaseManager(phaseName, doSomething, finishCondition, nextPhase)
--]]
myEnemy:setDefaultBullet("bullets.Laser", {laserFrame = "Lasers/2"})
function myEnemy:stage1()
logger:info(TAG, "The boss is in stage1, shoot 1 bullet")
--setup shoot
self.preTimer = self:addTimer(1000,
function()
self:shoot({x = self.x , degree = self.dir, speed = 100 * gameConfig.scaleFactor})
end
, -1)
end
function myEnemy:stage2()
logger:info(TAG, "The boss is in stage2, shoot 2 bullet")
self:cancelTimer(self.preTimer)
self.preTimer = self:addTimer(1000,
function()
self:shoot({x = self.x + self.width/4 , degree = self.dir, speed = 100 * gameConfig.scaleFactor})
self:shoot({x = self.x - self.width/4 , degree = self.dir, speed = 100 * gameConfig.scaleFactor})
end
, -1)
end
function myEnemy:stage3()
local counter = 0
logger:info(TAG, "The boss is in stage3, shoot 3 bullet, and a homing missile")
self:cancelTimer(self.preTimer)
self.preTimer = self:addTimer(1000,
function()
counter = counter + 1
self:shoot({x = self.x + self.width/4 , degree = self.dir, speed = 100 * gameConfig.scaleFactor})
self:shoot({x = self.x , degree = self.dir, speed = 100 * gameConfig.scaleFactor})
if counter%2 == 1 then
self:shoot({
x = self.x , degree = self.dir, speed = 100 * gameConfig.scaleFactor,
bulletClass = "bullets.Missile",
bulletOptions = {},
onShoot = function(bullet)
bullet:setScaleLinearVelocity(0, 200)
bullet:rotateTo(270)
move.seek(bullet, self.players[1])
end
})
end
self:shoot({x = self.x - self.width/4 , degree = self.dir, speed = 100 * gameConfig.scaleFactor})
end
, -1)
end
phaseManager:registerPhase(
"stage1",
myEnemy,
function()
return myEnemy.hp <= 666
end,
function()
return "stage2"
end
)
phaseManager:registerPhase(
"stage2",
myEnemy,
function()
return myEnemy.hp <= 333
end,
function()
return "stage3"
end
)
phaseManager:registerPhase(
"stage3",
myEnemy,
function()
return myEnemy.hp <= 0
end
)
phaseManager:setCurrentPhase("stage1")
--enemy is hurt
myEnemy:addEventListener("health", function(event)
--logger:info(TAG, "HP Event: name:%s, phase:%s, crime:%s, damage:%s, hp:%s", event.name, event.phase, event.crime.name or "", event.damage, event.hp)
phaseManager:check()
end)
function myEnemy:startAction()
phaseManager:start()
end
return myEnemy
end
return MyEnemy
使用自訂的魔王(Boss)
關卡設定
預設的無限模式中,遊戲會進行 10 次普通關卡,才會進入一次魔王關卡,如果你希望你的關卡能在無限模式中被當作為魔王關卡,必須要在關卡建立時在選項中設定 isBossFight = true
。以下的例子中,除了指定 isBossFight
,也指定了 bg
,為該關卡的預設背景因為,此為 0.986 版加入的功能。
local myLevel = Sublevel.new("9999999-086", "custom enemy", "author name", {isBossFight = true, bg = "bg"})
引用
由於魔王只是比較強壯的敵人,本質上和敵人並無不同,所以引用方式和之前引用敵人的範例是一樣的。要注意的是新增實體的方式,由於這個魔王會追蹤角色的行動,所以新增實體的函式中我們讓它必須帶入玩家的實體:players
。players
是所有的玩家,當開始雙人模式後,這個表格會包含兩個玩家的實體,分別為:players[1]
、players[2]
。反之單人模式中只會有一個玩家實體:players[1]。
local MyEnemy = require("levels.myLevel.MyBoss")
--some codes...
local enemy = MyEnemy.new({players = self.players})
HP Bar
由於魔王血量眾多,我們會需要將魔王血量顯示在 Hp Bar 上,提示玩家目前遊玩的進度。建立一個 HP Bar 的方式非常簡單,本模板提供了基本的 HpBar UI:ui.HpBar
。你只需要引用他並且創建一個新的 HP Bar 實體。其中參數 w
、h
、numLifes
、title
分別為 Hp Bar 的長、寬、血條數量、標題文字。
local HpBar = require("ui.HPBar")
-- some codes
local hpBar = HpBar.new({
w = gameConfig.contentWidth*0.88,
h = gameConfig.contentWidth*0.1,
numOfLifes = 3,
title = "Boss2"
})
新增完成之後我們將他放到螢幕上方:
hpBar.x = gameConfig.contentWidth / 2
hpBar.y = hpBar.height * 0.6
並且設置初始的血量:
hpBar:update(enemy.hp , enemy.maxHp)
也記得要在魔王血量發生變動時更新血量表:
local function checkHPBar(event)
if util.isExists(hpBar) then
hpBar:update(enemy.hp, enemy.maxHp)
end
end
--update hp bar when enemy is hurt
enemy:addEventListener("health", checkHPBar)
Hide/Show Score
由於螢幕上方需要顯示血量表,我們可以透過 Sublevel.game
控制器,暫時隱藏分數,除美觀之外,也避免玩家分心:
self.game:showScore(false)
也記得要在關卡結束時把分數顯示打開還原,不然下一關就看不到分數了:
self.game:showScore(true)
Warninig
注意魔王出現的時機,和一般敵人不同,魔王出現前會需要醞釀氣氛,而不是直接出現。所以我們會在魔王出現前顯示警告的訊息,當警告訊息過後,才出現魔王。我們可以直接呼叫 Sublevel
中的 showWarning
來顯示警告訊息:
function myLevel:show(options)
self:showWarning({
bg = "bg3",
onComplete = function()
self:initBoss()
end
})
end
其中 bg
為警告訊息過後會播放的背景音樂資源名稱,目前提供的音效資源如下:
資源 | 路徑 |
---|---|
bg | sounds/Juhani Junkala [Retro Game Music Pack] Level 1.mp3 |
bg2 | sounds/Juhani Junkala [Retro Game Music Pack] Level 2.mp3 |
bg3 | sounds/Juhani Junkala [Retro Game Music Pack] Level 3.mp3 |
bg4 | sounds/Juhani Junkala [Retro Game Music Pack] Ending.mp3 |
而 onComplete 則為警告訊息完成後會呼叫的函式,這裡我們呼叫 initBoss
來初始化我們的魔王,initBoss
是初始化魔王的地方,這裡我們先將魔王放置於螢幕外面,並透過 enemy.invincible = true
將我們的魔王設置成無敵,不然當魔王在螢幕外待命時被打成蜂窩就好笑了。接著透過給魔王一個速度並經過一段時間將魔王的速度設定為 0,這樣魔王便會過一段時間從螢幕外移動至螢幕內。當魔王移動到螢幕內後,將 enemy.invincible
設為 false
,讓魔王可以受到傷害:
enemy.x = gameConfig.contentWidth/2
enemy.y = -enemy.height/2
enemy.invincible = true
--some codes..
enemy:setScaleLinearVelocity(0, 200)
enemy:addTimer(1000, function()
enemy:setScaleLinearVelocity(0, 0)
--When the enemy is ready, the player can hurt it
enemy.invincible = false
enemy:startAction()
end)
結束條件
這裡的結束條件為:當 Boss 被打敗消失時即結束。和之前的結束條件不同的地方是,用到了另一個自己定義的變數 bossInited
,這是因為在遊戲的一開始 Boss 並未出現,而是在警告訊息過後才出現的。所以我們透過一個額外的變數來確認 boss 是否真的消失了,否則這個關卡會在一開始的時候便結束,陷入無限的迴圈。
function myLevel:isFinish()
--print("isFinish!??")
if not self.bossInited or util.isExists(self.enemy) then
return false
else
return true
end
end
由於該關卡可能會被重複執行,這裡選擇在 prepare()
內來進行每次 bossInited
變數的初始化:
function myLevel:prepare()
self.bossInited = false
end
全部的程式碼:
local gameConfig = require("gameConfig")
local Sublevel = require("Sublevel")
local MyEnemy = require("levels.myLevel.MyBoss")
local HpBar = require("ui.HPBar")
local util = require("util")
local myLevel = Sublevel.new("9999999-086", "custom enemy", "author name", {isBossFight = true, bg = "bg"})
function myLevel:show(options)
self:showWarning({
bg = "bg3",
onComplete = function()
self:initBoss()
end
})
end
function myLevel:prepare()
self.bossInited = false
end
function myLevel:initBoss()
local enemy = MyEnemy.new({players = self.players})
--Set the boss to be invisible at the beginning
enemy.invincible = true
self.bossInited = true
--set up hp bar
local hpBar = HpBar.new({
w = gameConfig.contentWidth*0.88,
h = gameConfig.contentWidth*0.1,
title = "Boss"
})
hpBar.x = gameConfig.contentWidth / 2
hpBar.y = hpBar.height * 0.6
hpBar:update(enemy.hp , enemy.maxHp)
--end of setting up hp bar
--place the enemy out of the screen
enemy.x = gameConfig.contentWidth/2
enemy.y = -enemy.height/2
--add Item to the enemy
enemy:addItem("items.PowerUp", {level = 1})
--destroy the enemy at the right time
enemy:autoDestroyWhenInTheScreen()
local function checkHPBar(event)
if util.isExists(hpBar) then
hpBar:update(enemy.hp, enemy.maxHp)
end
end
--update hp bar when enemy is hurt
enemy:addEventListener("health", checkHPBar)
--hide the score via the game controller
self.game:showScore(false)
enemy:setScaleLinearVelocity(0, 200)
enemy:addTimer(1000, function()
enemy:setScaleLinearVelocity(0, 0)
--When the enemy is ready, the player can hurt it
enemy.invincible = false
enemy:startAction()
end)
self.hpBar = hpBar
self.enemy = enemy
--insert to the scene
self:insert(hpBar)
self:insert(enemy)
end
function myLevel:finish()
self.game:showScore(true)
if self.hpBar then
self.hpBar:clear()
end
self.bossInited = false
end
function myLevel:isFinish()
--print("isFinish!??")
if not self.bossInited or util.isExists(self.enemy) then
return false
else
return true
end
end
return myLevel
多部位敵人 (Multiple Parts Enemy)
你可能看過某些遊戲的敵人是由多個部位所組成的,像一個巨大的機器人魔王,你要分別摧毀它的不同部位:像是手,腳,頭等等,才算是擊敗它。
建立一個多部位敵人
這邊提供一個簡單的方法讓你達成類似的功能,下面的例子中,我們將三個敵人當作部位零件組合起來,讓他變成一個比較大的敵人,當玩家將三個部位全部消滅,才算打敗這個組合起來的敵人。
由於 Corona SDK 物理引擎的限制,碰撞的物體必須在同一個群體 (Display Group)中,所以我們在這個模組的建構式多一個參數 parent
,傳入這個敵人存在的群體,將稍後新增的敵人加入同一個群體:
Enemy.new = function(parent, options)
--...
end
首先我們先用敵人的模組來建立這個三個部位,並且賦予它們 100 點的血量:
local myEnemy1 = MyEnemy.new()
local myEnemy2 = MyEnemy.new()
local myEnemy3 = MyEnemy.new()
myEnemy1.hp = 100
myEnemy2.hp = 100
myEnemy3.hp = 100
記得將部位加入指定的群體:
parent:insert(object)
parent:insert(myEnemy1)
parent:insert(myEnemy2)
parent:insert(myEnemy3)
為了讓它們能一起移動,我們要建立一個這個組合敵人的核心,讓這三個部位以這個核心為基準點進行移動,這個核心不能被摧毀,所以我們使用不會跟任何其它物件碰撞的 GameObject 來實作它:
local object = GameObject.new(options)
local rect = display.newRect(0, 0, 100, 100)
rect.alpha = 0
object:insert(rect)
object:enablePhysics()
注意這裡我們在核心物件中加入了一個完全透明的矩形,這樣啟動物理引擎的時候才能自動描繪出物理身體,有了物理身體我們才能讓這個核心使用物理的方式移動。為了能讓其他部位能跟隨著核心移動,我們使用 該核心中的 enterFrame
,在每幀的時候移動我們的將其它部位移動到正確的位置,注意 enterFrame 方法會在他的擁有者被回收時跟著一起被回收,你不會擔心他會一直留在遊戲中。
object.enterFrame:each(function()
if util.isExists(myEnemy1) then
myEnemy1.x = object.x + myEnemy1.width
myEnemy1.y = object.y
end
if util.isExists(myEnemy2) then
myEnemy2.x = object.x - myEnemy2.width
myEnemy2.y = object.y
end
if util.isExists(myEnemy3) then
myEnemy3.x = object.x
myEnemy3.y = object.y
end
end)
除了讓各個部位可以一起移動,我們還要偵測三個部位的存活狀態,視情況判定這個組合起來的敵人是否死亡,所以我們在每個敵人註冊 health 事件,他會在敵人血量發生變化以及死亡時發出通知,且在每次有部位被摧毀時,使用 object:checkDead()
來確認這個組合型敵人是否死亡:
myEnemy1:addEventListener("health", function(evnet)
if evnet.phase == "dead" then
object:checkDead()
end
end)
myEnemy2:addEventListener("health", function(evnet)
if evnet.phase == "dead" then
object:checkDead()
end
end)
myEnemy3:addEventListener("health", function(evnet)
if evnet.phase == "dead" then
object:checkDead()
end
end)
object:checkDead()
會去紀錄每次死亡的次數,當死亡次數達到 3,代表所有部位被摧毀。當所有部位被摧毀的時候,摧毀核心並發出爆炸的特效。
object.deadCount = 0
function object:checkDead()
self.deadCount = self.deadCount + 1
if self.deadCount == 3 then
local effect = Effect.new({
time = 800
})
effect.x = self.x
effect.y = self.y
effect:start()
self:clear()
end
end
全部的程式碼:
--/levels/myLevel/MyMultipartEnemy
local util = require("util")
local GameObject = require("GameObject")
local MyEnemy = require("levels.myLevel.MyEnemy")
local Effect = require("effects.pixelEffect1")
local Enemy = {}
Enemy.new = function(parent, options)
local object = GameObject.new(options)
local rect = display.newRect(0, 0, 100, 100)
rect.alpha = 0
object:insert(rect)
object:enablePhysics()
local myEnemy1 = MyEnemy.new()
local myEnemy2 = MyEnemy.new()
local myEnemy3 = MyEnemy.new()
myEnemy1.hp = 100
myEnemy2.hp = 100
myEnemy3.hp = 100
parent:insert(object)
parent:insert(myEnemy1)
parent:insert(myEnemy2)
parent:insert(myEnemy3)
object.deadCount = 0
myEnemy1:addEventListener("health", function(evnet)
if evnet.phase == "dead" then
object:checkDead()
end
end)
myEnemy2:addEventListener("health", function(evnet)
if evnet.phase == "dead" then
object:checkDead()
end
end)
myEnemy3:addEventListener("health", function(evnet)
if evnet.phase == "dead" then
object:checkDead()
end
end)
object.enterFrame:each(function()
if util.isExists(myEnemy1) then
myEnemy1.x = object.x + myEnemy1.width
myEnemy1.y = object.y
end
if util.isExists(myEnemy2) then
myEnemy2.x = object.x - myEnemy2.width
myEnemy2.y = object.y
end
if util.isExists(myEnemy3) then
myEnemy3.x = object.x
myEnemy3.y = object.y
end
end)
function object:checkDead()
self.deadCount = self.deadCount + 1
if self.deadCount == 3 then
local effect = Effect.new({
time = 800
})
effect.x = self.x
effect.y = self.y
effect:start()
self:clear()
end
end
return object
end
return Enemy
使用這個敵人
使用這個敵人的方式和使用其他敵人模組大同小異,只是必須將該關卡存放物件的群體(Display Group)帶入,即是 self.view
:
local MyEnemy = require("levels.myLevel.MyMultipartEnemy")
local enemy = MyEnemy.new(self.view)
另一點值得注意的是我們並沒有將這個敵人的自動回收機制打開,因為該敵人的核心很小,使用自動回收機制的話,會在敵人還沒完全離開螢幕時就將敵人回收了。所以這邊讓敵人不斷的左右移動,除非敵人被摧毀,否則不會離開關卡。
這邊移動的方式比較透別,透過一個 timer 去累加數值,並根據該數值的不同設定不同的移動速度與方向:
enemy:addTimer(1000, function()
count = count + 1
self:moveMyEnemy(enemy, count)
end, -1)
在 count
每第二次發生變化的時候,改變移動方向,讓它可以左右移動:
function myLevel:moveMyEnemy(enemy, count)
if count%4 == 0 then
elseif count%4 == 1 then
enemy:setScaleLinearVelocity(200, 0)
elseif count%4 == 2 then
elseif count%4 == 3 then
enemy:setScaleLinearVelocity(-200, 0)
end
count = count + 1
end
全部的程式碼:
--level_multipart_enemy
local gameConfig = require("gameConfig")
local Sublevel = require("Sublevel")
local MyEnemy = require("levels.myLevel.MyMultipartEnemy")
local util = require("util")
local myLevel = Sublevel.new("9999999-061", "custom enemy", "author name")
function myLevel:show(options)
local enemy = MyEnemy.new(self.view)
--place the enemy out of the screen
enemy.x = gameConfig.contentWidth/2
enemy.y = -enemy.height
enemy:setScaleLinearVelocity( 0, 100 )
enemy:addTimer(1000, function()
local count = 0
enemy:setScaleLinearVelocity(-200, 0)
self:moveMyEnemy(enemy, count)
enemy:addTimer(1000, function()
count = count + 1
self:moveMyEnemy(enemy, count)
end, -1)
end)
self.enemy = enemy
end
function myLevel:moveMyEnemy(enemy, count)
-- print("Timer is work!")
if count%4 == 0 then
elseif count%4 == 1 then
enemy:setScaleLinearVelocity(200, 0)
elseif count%4 == 2 then
elseif count%4 == 3 then
enemy:setScaleLinearVelocity(-200, 0)
end
count = count + 1
end
function myLevel:isFinish()
--print("isFinish!??")
if util.isExists(self.enemy) then
return false
else
return true
end
end
return myLevel