生成是指在体验中创建对象或角色的过程,而重生是指在角色满足某种移除条件(例如角色的生命值降至零或掉出地图)后,将对象或角色重新添加到体验中的过程。这两个过程都很重要,因为它们确保玩家能够加入您的体验,并且可以继续玩,以提高他们的技能。
使用 示例激光标签体验 作为参考,本节教程将教您如何使用和自定义 Roblox 内置功能来处理生成和重生,包括关于以下内容的脚本指导:
- 配置生成位置,使玩家只能生成到其团队的生成区。
- 将新玩家及其角色添加到回合中,因为他们加入体验。
- 定制在玩家生成和重生时防止伤害的力场。
- 处理客户端状态,以便在适当的时候确保游戏玩法正常。
- 在角色被标记出局后进行重生。
- 执行小的杂项操作,这对设置游戏玩法和角色参数至关重要。
本节包含大量脚本内容,但在创建体验时,而不是从头开始编写所有内容,它鼓励您利用现有组件,快速迭代,并找出哪些系统需要定制实现以符合您的愿景。完成本节后,您将学习如何实现基于回合的游戏玩法,该游戏玩法跟踪积分,监控玩家状态并显示回合结果。
配置生成位置
如果您现在进行体验测试,所有玩家将随机在绿色团队的生成区或粉色团队的生成区的 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 的 Playtest 中选择至少两个角色来重现玩家的体验,这是一种 多客户端模拟。

另一个 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 值得讨论。此表存储所有玩家及其 Connections 到 GetAttributeChangedSignal。将该连接存储在表中的原因是为了便于 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() 在整个体验中被频繁调用:例如,在回合的初始设置期间,在玩家选择爆破器后以及当玩家返回到大厅或 中立 生成位置时。此外,在玩家选择爆破器后,ServerScriptService ⟩ BlasterSelectedHandler 将 PlayerState 设置为 Playing,PlayerStateHandler 最终可以通过调用 scheduleDestroyForceField() 来移除力场。
自定义力场
示例激光标签体验使用 Studio 内置的 ForceField 类来防止玩家在选择爆破器时受到伤害,而不是使用自定义实现。这确保了玩家在生成时只需在生成位置中包含一个 SpawnLocation.Duration 属性大于 0 的状态便可以使用力场。示例使用了一个任意值 9999 来启用力场,然后在 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,它检查三个独特的条件:
- 在玩家选择爆破器后,力场需要持续长 enough,以允许玩家适应周围环境。
- 在此适应时间内,力场不能成为优势,因此它们在玩家发射爆破器的瞬间必须消失。
- 当玩家重置角色时,无论是发射前还是力场超时之前,力场也需要消失。
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 属性的变化触发独特行为。具体来说,它禁用玩家移动,呈现重生 UI,并禁用爆破器。
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 中设置一次,并修改控件和 UI 以补偿视角的变化。
现在玩家可以生成、选择爆破器并从第一人称视角瞄准,下一节将教您创建基于回合的游戏玩法背后的脚本。