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


當玩家加入體驗時,ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ spawnPlayersInMap 會檢查每個團隊中已經有多少名玩家,然後返回擁有最少玩家的團隊。
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,您稍後將在教程中了解更多內容。
spawnPlayersInMap
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 } 表中的玩家,腳本:
- 將玩家的 Player.Neutral 屬性設置為 true,以自動重置其 Player.Team 爲 nil,允許玩家在回合不活躍時在休息室重生,因為生成位置的 Neutral 屬性也設置為 true。
- 將其 PlayerState 更改為 InLobby,以移除玩家的標槍和第一人稱的用戶界面視覺效果。
欲了解有關中立生成區域及其在每個回合中功能的更多信息,請參閱下一節教程中的添加回合。
spawnPlayersInLobby
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 進行自定義設置。在設置這些值後,腳本等待角色的生命值降為零。您稍後將在本節中了解更多關於重生的內容。
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 狀態,這是玩家在生產出來的競技場中可以從兩個不同的標槍類型中選擇的回合的初始狀態。這個狀態還包括一個保護場,防止玩家在進行選擇時受到傷害。
PlayerStateHandler
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。這個表格存儲所有玩家及其與 GetAttributeChangedSignal 的 Connections 連接。將此連接存儲在表中的原因是,PlayerStateHandler 可以在玩家離開體驗時斷開它。這個過程作為一種內存管理來防止連接數量隨著時間的推移而不斷增長。
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。在稍後的實現標槍教程中,您將獲得更多有關此過程的詳細信息。
PlayerStateHandler
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 中的六邊形腳本。
Commenting Out Properties in ForceFieldClientVisuals
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() 以滿足這些條件。
scheduleDestroyForceField
-- 將保護場結束,如果玩家開火
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 布林值確保該函數僅嘗試銷毀一次保護場。
scheduleDestroyForceField
local function endForceField()
if forceFieldEnded then
return
end
forceFieldEnded = true
attributeChangedConnection:Disconnect()
characterRespawnedConnection:Disconnect()
destroyForceField(player)
end
處理客戶端狀態
雖然本節大部分重點放在 ServerScriptService ⟩ PlayerStateHandler 上,但在 ReplicatedStorage 中還有一個同名的腳本。之所以拆分,是因為客戶端-伺服器架構:
客戶端需要了解玩家狀態信息,以便能夠實時作出相應的反應,例如顯示正確的用戶界面元素,或允許玩家移動和開火。
伺服器需要所有相同的信息,以便能夠防止作弊。例如,伺服器也需要玩家狀態來執行像生成和裝備角色、禁用保護場和顯示排行榜等操作。這就是為什麼這個腳本在 ReplicatedStorage 中,而不是純粹的客戶端位置。
要查看這個核心邏輯,請檢查 ReplicatedStorage ⟩ PlayerStateHandler 中的以下腳本,該腳本驗證用戶的當前狀態,然後調用相應的函數來處理該狀態的相應操作。
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
所有事件響應在此腳本中邏輯上分組在一起,因為它們需要相似的行為來啟用或禁用玩家控制、相機運動,以及可見的用戶界面層。例如,在標槍選擇期間,玩家需要同時無敵且不能移動。伺服器已經處理了保護場,但客戶端處理了移動。為了演示,如果您檢查 onSelectingBlaster() 函數的邏輯,您會看到客戶端在玩家選擇標槍時禁用了玩家的移動。
PlayerStateHandler
local function onSelectingBlaster()
togglePlayerCamera(true)
togglePlayerMovement(false)
setGuiExclusivelyEnabled(playerGui.PickABlasterGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
end
onPlaying() 函數也同樣簡單。它啟用移動,過渡到主要的頭上顯示(HUD),啟用標槍,並調用與服務器相同的保護場函數。
PlayerStateHandler
local function onPlaying()
togglePlayerMovement(true)
setGuiExclusivelyEnabled(playerGui.HUDGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Ready)
scheduleDestroyForceField()
end
重生角色
示例激光標籤體驗通過 ReplicatedStorage ⟩ PlayerStateHandler 中的 onTaggedOut() 狀態來處理角色重新生成回合。與 onSelectingBlaster() 和 onPlaying() 狀態一樣,onTaggedOut() 根據 playerState 屬性的變化觸發獨特的行為。具體來說,它禁用玩家移動,顯示重生用戶界面,並禁用標槍。
PlayerStateHandler
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。
SetupHumanoidlocal Players = game:GetService("Players")Players.RespawnTime = 10 -- 新行,單位為秒
雜項設置
作為初始設置的一部分,示例激光標籤體驗還執行了一些小但關鍵的步驟:
該體驗包含一個名為 StarterPlayer ⟩ StarterCharacterScripts ⟩ Health 的空腳本,該腳本禁用預設的Roblox健康再生。要了解此屬性的行為,請參見 Humanoid.Health。
該體驗通過設置 StarterPlayer.CameraMode.LockFirstPerson 屬性來使用第一人稱相機。請注意,如果希望用戶在第一人稱和第三人稱相機之間切換,則必須以編程方式而非僅在Studio中設置該屬性,並修改控制和用戶界面以補償視角的變化。
現在玩家可以生成、選擇標槍並從第一人稱視角瞄準,下一節將教您有關創建基於回合的遊戲玩法的腳本。