生成是指在遊戲中創建物件或角色的過程,而重生是指在滿足移除條件後,將物件或角色重新加入遊戲的過程,例如角色的生命值降至零或掉落地圖。這兩個過程都很重要,因為它們確保玩家能夠加入你的遊戲,並能繼續遊玩以提升他們的技能。
以範例激光標籤遊戲為參考,本教程的這一部分教你如何使用和自定義Roblox的內建功能來處理生成和重生,包括腳本指導:
- 配置生成位置,以便玩家只能在他們隊伍的生成區域生成。
- 當新玩家加入遊戲時,將他們和他們的角色添加到回合中。
- 自定義防護場,以防止玩家在生成和重生時受到傷害。
- 處理客戶端狀態,以便在適當的時間正確運行遊戲。
- 在角色被標記出局後重生角色。
- 執行一些小的雜項操作,這些操作對於設置遊戲和角色參數至關重要。
這一部分包含大量的腳本內容,但在創建遊戲時,鼓勵你利用現有的組件,快速迭代,並找出哪些系統需要自定義實現以符合你的願景。完成這一部分後,你將學會如何實現基於回合的遊戲玩法,跟蹤分數,監控玩家狀態,並顯示回合結果。
配置生成位置
如果你現在進行遊戲測試,所有玩家將隨機在綠隊的SpawnLocation物件或粉隊的SpawnLocation物件中生成。這會造成一個遊戲問題,因為玩家可以在對方的生成區域內互相標記,當對手的防護場消失時。
為了解決這個問題,範例激光標籤遊戲將兩個生成位置的Neutral屬性設置為false,以限制對方隊伍的玩家在錯誤的生成區域生成,並將TeamColor屬性設置為來自分配隊伍顏色的相應Team.Color值:


當玩家加入遊戲時,ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ spawnPlayersInMap會檢查每個隊伍中已經有多少玩家,然後返回玩家最少的隊伍。
local function getSmallestTeam(): Team
local teams = Teams:GetTeams()
-- 按從小到大的順序對隊伍進行排序
table.sort(teams, function(teamA: Team, teamB: Team)
return #teamA:GetPlayers() < #teamB:GetPlayers()
end)
-- 返回最小的隊伍
return teams[1]
end一旦知道了玩家最少的隊伍,它就會將玩家分配到該隊伍,將他們的Player.Neutral屬性設置為false,以便玩家只能在他們隊伍的生成位置生成和重生,然後將他們的PlayerState設置為SelectingBlaster,你將在本教程的後面部分了解更多。
local function spawnPlayersInMap(players: { Player })
for _, player in players do
player.Team = getSmallestTeam()
player.Neutral = false
player:SetAttribute(PlayerAttribute.playerState, PlayerState.SelectingBlaster)
task.spawn(function()
player:LoadCharacter()
end)
end
end如果你檢查Workspace ⟩ World ⟩ Map ⟩ Spawns,你會看到地圖中還有一個生成位置:NeutralSpawn。這個生成位置與其他位置不同,因為它沒有將TeamColor屬性設置為遊戲中的兩個隊伍之一;相反,這個生成位置的Neutral屬性會根據回合是否活躍而改變。
例如,如果回合是活躍的,Neutral屬性設置為false,以便spawnPlayersInMap可以將玩家分配到隊伍並將他們生成到競技場中。然而,如果回合不活躍,例如在一回合與下一回合之間的時間,Neutral屬性設置為true,以便玩家可以在那裡生成,而不管他們的隊伍狀態如何。這個過程使得Neutral生成位置成為一個功能性大廳。

為了演示,如果你檢查ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ SpawnPlayersInLobby,該腳本在回合結束時運行,你會看到對於傳遞到players: { Player }表中的每個玩家,腳本:
- 將他們的PlayerState更改為InLobby,以移除玩家的槍和第一人稱UI視覺效果。
有關中立生成區及其在每回合中的功能的更多信息,請參見本教程下一部分的添加回合。
local function spawnPlayersInLobby(players: { Player })
for _, player in players do
player.Neutral = true
player:SetAttribute(PlayerAttribute.playerState, PlayerState.InLobby)
task.spawn(function()
player:LoadCharacter()
end)
end
end連接新玩家
Studio中的Luau代碼通常是事件驅動的,這意味著腳本會監聽來自Roblox服務的事件,然後根據事件調用函數。例如,在將新玩家添加到多人遊戲時,必須有一個事件來處理所有必要的操作,以便玩家能夠成功連接。在範例激光標籤遊戲中,這個對應的事件是Players.PlayerAdded:Connect。
Players.PlayerAdded:Connect是遊戲中多個腳本的一部分。如果你使用Ctrl/Cmd+Shift+F快捷鍵並搜索Players.PlayerAdded:Connect,結果將為理解遊戲的初始設置提供一個良好的起點。

為了演示,打開ServerScriptService ⟩ SetupHumanoid。Player和Character之間的區別是理解這個腳本的關鍵:
- 玩家需要選擇一把槍並被添加到排行榜中。角色需要生成並獲得一把槍。
SetupHumanoid立即檢查玩家是否有角色(剛加入)或沒有(正在重生)。在找到一個角色後,它調用onCharacterAdded(),從角色中獲取Humanoid模型,並將其傳遞到ServerScriptService ⟩ SetupHumanoid ⟩ setupHumanoidAsync進行自定義。在設置這些值後,腳本會等待角色的生命值降至零。你將在本教程的後面部分了解更多有關重生的內容。
local function setupHumanoidAsync(player: Player, humanoid: Humanoid)
humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.Subject
humanoid.NameDisplayDistance = 1000
humanoid.HealthDisplayDistance = 1000
humanoid.NameOcclusion = Enum.NameOcclusion.OccludeAll
humanoid.HealthDisplayType = Enum.HumanoidHealthDisplayType.AlwaysOn
humanoid.BreakJointsOnDeath = false
humanoid.Died:Wait()
onHumanoidDied(player, humanoid)
end這個腳本的重要說明是,這些屬性都是完全可選的,這意味著如果你刪除函數的前六行,遊戲仍然能正常運行。這些屬性並不是功能要求,而是允許你做出符合遊戲目標的設計決策。例如:
- 如果你希望角色名稱在更近的距離顯示,減少Humanoid.NameDisplayDistance的值。
- 如果你只希望角色的生命值在低於100%時顯示,將Humanoid.HealthDisplayType設置為DisplayWhenDamaged。
- 如果你希望角色在生命值降至0時分崩離析,將Humanoid.BreakJointsOnDeath設置為True。
如果你更改這些屬性的值,進行遊戲測試是很重要的,以便你能看到新設置的影響。你可以通過在Server & Clients的遊戲測試中選擇至少兩個角色來重現玩家的體驗,進行多客戶端模擬。

Players.PlayerAdded:Connect事件的另一個示例在ServerScriptService ⟩ PlayerStateHandler中。與前面的示例一樣,PlayerStateHandler立即檢查角色。如果玩家不在大廳,腳本將玩家屬性設置為SelectingBlaster狀態,這是回合的初始狀態,玩家可以在進入競技場後從兩種不同的槍中選擇一種。這個狀態還包括一個防護場,防止玩家在選擇時受到傷害。
local function onPlayerAdded(player: Player)
player.CharacterAdded:Connect(function()
if not player.Neutral then
player:SetAttribute(PlayerAttribute.playerState, PlayerState.SelectingBlaster)
onPlayerStateChanged(player, PlayerState.SelectingBlaster)
end
end)在PlayerStateHandler中的一個特定變數值得討論:attributeChangedConnectionByPlayer。這個表存儲所有玩家及其Connections到GetAttributeChangedSignal。將這個連接存儲在表中的原因是,PlayerStateHandler可以在玩家離開遊戲時斷開它。這個過程作為一種記憶管理,以防止連接數量隨著時間的推移而不斷增長。
local attributeChangedConnectionByPlayer = {}
local function onPlayerAdded(player: Player)
-- 處理所有未來的玩家狀態更新
attributeChangedConnectionByPlayer[player] = player
:GetAttributeChangedSignal(PlayerAttribute.playerState)
:Connect(function()
local newPlayerState = player:GetAttribute(PlayerAttribute.playerState)
onPlayerStateChanged(player, newPlayerState)
end)
end
-- 當玩家離開時斷開屬性變更連接
local function onPlayerRemoving(player: Player)
if attributeChangedConnectionByPlayer[player] then
attributeChangedConnectionByPlayer[player]:Disconnect()
attributeChangedConnectionByPlayer[player] = nil
end
end你可以看到onPlayerAdded()中的兩個連接函數都調用了onPlayerStateChanged()。在玩家排序到隊伍後的初始設置中,onPlayerAdded()將PlayerState設置為SelectingBlaster,因此第一個if語句的評估為false,並禁用BlasterState。在後面的實現槍械部分中,你將了解有關此過程的更多細節。
local function onPlayerStateChanged(player: Player, newPlayerState: string)
-- 只有當玩家狀態為'Playing'時,槍械狀態才為'Ready'
local newBlasterState = if newPlayerState == PlayerState.Playing then BlasterState.Ready else BlasterState.Disabled
-- 當玩家開始遊玩時安排銷毀防護場的邏輯
if newPlayerState == PlayerState.Playing then
scheduleDestroyForceField(player)
end
player:SetAttribute(PlayerAttribute.blasterStateServer, newBlasterState)
end如果你添加斷點或甚至只是一個print()語句,你可以看到onPlayerStateChanged()在遊戲中經常被調用:例如在回合的初始設置期間,將其設置在主代碼路徑上,玩家選擇槍械後,以及當玩家返回大廳或Neutral生成位置時。此外,在玩家選擇槍械後,ServerScriptService ⟩ BlasterSelectedHandler將PlayerState設置為Playing,而PlayerStateHandler最終可以通過調用scheduleDestroyForceField()來移除防護場。
自定義防護場
範例激光標籤遊戲使用Studio的內建ForceField類來防止玩家在選擇槍械時受到傷害,而不是使用自定義實現。這確保了玩家生成時只需包含SpawnLocation.Duration屬性大於0的生成位置即可啟用防護場。範例使用了一個任意值9,999來啟用防護場,然後在ReplicatedStorage ⟩ ForceFieldClientVisuals中以編程方式處理實際持續時間。
類似於setupHumanoidAsync,ForceFieldClientVisuals中的大多數行都是可選的。例如,如果你像以下腳本那樣註釋掉函數的內容,遊戲將使用默認的閃爍防護場,而不是StarterGui ⟩ ForceFieldGui中的六邊形腳本。
local function onCharacterAddedAsync(character: Model)
-- local forceField = character:WaitForChild("ForceField", 3)
-- if not forceField then
-- return
-- end
-- forceField.Visible = false
-- localPlayer.PlayerGui:WaitForChild("ForceFieldGui").Enabled = true
-- forceField.Destroying:Wait()
-- localPlayer.PlayerGui.ForceFieldGui.Enabled = false
end由於自定義防護場是一個GUI,而不是新的ParticleEmitter,因此ForceFieldClientVisuals腳本僅影響每個玩家的第一人稱視覺效果,而不是玩家在查看其他玩家時的第三人稱視覺效果。第三人稱視覺效果保留了Roblox的默認外觀。要了解有關修改防護場的更多信息,請參見ForceField.Visible。


防護場是有用的,因為它們為玩家在生成和重生之間提供了足夠的時間,而無需擔心敵方玩家,但最終它們需要消失以進行主要的激光標籤遊戲。處理防護場移除的腳本位於ReplicatedStorage ⟩ scheduleDestroyForceField,並檢查三個獨特的條件:
- 玩家選擇槍械後,防護場需要持續足夠長的時間,以便玩家適應周圍環境。
- 在這段適應時間內,防護場不能成為優勢,因此它們需要在玩家開火的瞬間消失。
- 當玩家重置角色時,無論是在開火之前還是防護場超時之前,防護場都需要消失。
scheduleDestroyForceField腳本中的每個檢查都調用endForceField()以滿足這些條件。
-- 如果玩家開火則結束防護場
local blasterStateAttribute = getBlasterStateAttribute()
attributeChangedConnection = player:GetAttributeChangedSignal(blasterStateAttribute):Connect(function()
local currentBlasterState = player:GetAttribute(blasterStateAttribute)
if currentBlasterState == BlasterState.Blasting then
endForceField()
end
end)
-- 如果玩家重置則結束防護場
characterRespawnedConnection = player.CharacterRemoving:Connect(endForceField)
-- 在8秒後結束防護場
task.delay(MAX_FORCE_FIELD_TIME, endForceField)endForceField()包含一個看似奇怪的if語句,圍繞forceFieldEnded布林值。由於檢查是按順序運行的,腳本可以多次調用endForceField()函數。forceFieldEnded布林值確保該函數僅嘗試銷毀防護場一次。
local function endForceField()
if forceFieldEnded then
return
end
forceFieldEnded = true
attributeChangedConnection:Disconnect()
characterRespawnedConnection:Disconnect()
destroyForceField(player)
end處理客戶端狀態
雖然本節的大部分內容集中在ServerScriptService ⟩ PlayerStateHandler上,但在ReplicatedStorage中還有一個同名的腳本。分開的原因是客戶端-伺服器架構:
客戶端需要理解玩家狀態信息,以便能夠實時做出適當的反應,例如顯示正確的用戶界面元素,或使玩家能夠移動和開火。
伺服器需要所有這些相同的信息,以便能夠防止利用漏洞。例如,伺服器還需要玩家狀態來執行像生成和裝備角色、禁用防護場和顯示排行榜等操作。這就是為什麼這個腳本在ReplicatedStorage中,而不是純粹的客戶端位置。
要查看這個核心邏輯,請檢查ReplicatedStorage ⟩ PlayerStateHandler中的以下腳本,該腳本驗證用戶的當前狀態,然後調用相應的函數來處理該狀態的相應操作。
local function onPlayerStateChanged(newPlayerState: string)
if newPlayerState == PlayerState.SelectingBlaster then
onSelectingBlaster()
elseif newPlayerState == PlayerState.Playing then
onPlaying()
elseif newPlayerState == PlayerState.TaggedOut then
onTaggedOut()
elseif newPlayerState == PlayerState.InLobby then
onInLobby()
else
warn(`無效的玩家狀態 ({newPlayerState})`)
end
end所有事件響應在這個腳本中邏輯上被分組在一起,因為它們需要類似的行為來啟用或禁用玩家控制、相機移動以及可見的UI層。例如,在槍械選擇期間,玩家需要同時無敵且無法移動。伺服器已經處理了防護場,但客戶端處理移動。為了演示,如果你檢查onSelectingBlaster()函數的邏輯,你會看到客戶端在玩家選擇槍械時禁用玩家移動。
local function onSelectingBlaster()
togglePlayerCamera(true)
togglePlayerMovement(false)
setGuiExclusivelyEnabled(playerGui.PickABlasterGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
endonPlaying()函數同樣簡單。它啟用移動,過渡到主顯示界面(HUD),啟用槍械,並調用與伺服器相同的防護場函數。
local function onPlaying()
togglePlayerMovement(true)
setGuiExclusivelyEnabled(playerGui.HUDGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Ready)
scheduleDestroyForceField()
end重生角色
範例激光標籤遊戲通過ReplicatedStorage ⟩ PlayerStateHandler中的onTaggedOut()狀態處理角色的重生。與onSelectingBlaster()和onPlaying()狀態一樣,onTaggedOut()根據playerState屬性的變化觸發獨特的行為。具體來說,它禁用玩家移動,顯示重生UI,並禁用槍械。
local function onTaggedOut()
-- 標記出局時禁用控制
togglePlayerMovement(false)
togglePlayerCamera(false)
setGuiExclusivelyEnabled(playerGui.OutStateGui)
-- 標記出局時禁用槍械
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
end如果你想測試這種行為,可以按Esc,導航到設置選項卡,然後單擊重置角色按鈕。注意當你觸發重生畫面時,你無法移動、旋轉相機或開火。


重要的是要注意,這個腳本實際上並不重生角色,它只是停止他們的行動並向玩家提供視覺反饋,告訴他們伺服器正在重生他們的角色。為了演示,如果你檢查ServerScriptService ⟩ SetupHumanoid ⟩ setupHumanoidAsync ⟩ onHumanoidDied,腳本將PlayerState設置為TaggedOut(本質上通知ReplicatedStorage ⟩ PlayerStateHandler),並添加一些視覺指示。實際的重生邏輯是Roblox的內建行為。
當玩家重生回合時,他們會根據SpawnLocation.TeamColor屬性在他們隊伍的生成位置重生。要自定義重生時間,你可以在SetupHumanoid的頂部添加以下行。要了解有關此技術的更多信息,請參見Players.RespawnTime。
local Players = game:GetService("Players")
Players.RespawnTime = 10 -- 新行,單位為秒雜項設置
作為初始設置的一部分,範例激光標籤遊戲還執行了一些小但關鍵的步驟:
遊戲包括一個名為StarterPlayer ⟩ StarterCharacterScripts ⟩ Health的空腳本,該腳本禁用默認的Roblox生命值再生。要了解此屬性的行為,請參見Humanoid.Health。
遊戲通過設置StarterPlayer.CameraMode.LockFirstPerson屬性來使用第一人稱相機。請注意,如果你希望讓用戶在第一人稱和第三人稱相機之間切換,則必須以編程方式更改該屬性,而不僅僅是在Studio中設置一次,並修改控制和UI以補償視角的變化。
現在玩家可以生成、選擇槍械並從第一人稱視角瞄準,下一部分將教你有關創建基於回合的遊戲玩法背後的腳本。