生成 是在体验中创建对象或角色的过程,而 重生 是在体验满足移除条件后将对象或角色添加回体验的过程,例如角色的生命值达到零或掉出地图。这两个过程都很重要,因为它们确保玩家能够加入你的体验,并可以继续游玩以提高技能。
使用 样本激光标签体验 作为参考,本教程的这一部分教你如何使用和自定义 Roblox 内置功能来处理生成和重生,包括关于脚本指南的指导:
- 配置生成位置,让玩家只能生成到他们的团队的生成区域。
- 添加新玩家和他们的角色到回合中,因为他们加入体验。
- 自定义防止玩家伤害生成和重生的力场。
- 处理客户端状态,以便游戏在适当的时间正确运行。
- 在回合结束后重生角色。
- 执行小而杂的动作,这些动作对设置游戏和角色参数至关重要。
本节包含大量脚本内容,但在创建体验时,而不是一次从零开始写所有内容,它鼓励您利用现有组件,快速迭代,并确定哪些系统需要定制实现来匹配您的愿景。完成本节后,您将学习如何实现基于回合的游戏玩法,该玩法可追踪积分、监控玩家状态并显示回合结果。
配置生成位置
如果你现在玩测试体验,所有玩家将随机生成在绿团队的生成区或粉团队的生成区中的 SpawnLocation 对象,或在任何生成区中的 SpawnLocation 对象。这导致玩家可以在他们的对手的力场消失后立即在每个生成区域内标记对方。
为了解决这个问题,样本激光标签体验配置了两个生成位置,将 Neutral 属性设置为 false 来限制对方团队玩家在错误的生成区域生成,以及 TeamColor 属性设置为相应的 Team.Color 值从 分配团队颜色 教程的上一节中的相应部分:


当玩家加入体验时, 服务器脚本服务 > 游戏 > 回合 > 在地图上生成玩家 检查每个团队中有多少玩家,然后返回最少数玩家的团队。
在地图上生成玩家
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
如果你检查 工作区 > 世界 > 地图 > 生成 ,你可以看到地图上还有一个生成位置: 中立生成 。这个生成位置与其他位置不同,因为它没有将属性设置为体验中的两个团队之一;相反,这个生成位置有一个 属性,它会根据是否启用回合而变化。
例如,如果回合已激活,那么 Neutral 属性将设置为 false 因此 spawnPlayersInMap 可以将玩家排成队伍并将他们生成到竞技场。然而,如果回合未激活,例如一个回合与下一个回合之间的时间,那么 Neutral 属性将设置为 true ,因此玩家无论他们的团队状态如何都可以在那里生成。这个过程是使 中立 生成位置成为功能性大厅的原因。

要展示,如果你检查 ServerScriptService > 游戏玩法 > 回合 > 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 ,结果为了解体验的初始设置提供了良好的起点。

要展示,打开 服务器脚本服务 > 设置人形 。区分 Player 和 Character 是理解此脚本的关键:
- 玩家需要选择爆破器并被添加到排行榜上。角色需要生成并接收爆破器。
SetupHumanoid 立即检查玩家是否有角色(刚刚加入)或没有(正在重生)。找到一个后,它调用 onCharacterAdded() ,从角色那里获取 Humanoid 模型,然后传给 ServerScriptService > 设置人形 > 设置人形异步 进行自定义。设置这些值之后,脚本然后等待角色的生命值达到零。您将在本教程的此部分中了解更多关于重生的内容。
设置人形异步
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 设置为 在受伤时显示 。
- 如果你想让角色在生命值达到 0 时分裂,将 Humanoid.BreakJointsOnDeath 设置为 真实 。
如果您更改这些属性的值,很重要进行游戏测试,以便您可以看到新设置的影响。您可以在 测试 选项卡的 客户端和服务器 部分中选择至少两个角色来重现玩家在多人游戏环境中的体验。

另一个 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 声明被评为为错误并禁用了 BlasterState 。在教程的后期 实现爆破器 部分,你将学到更多关于这个过程的细节。
玩家状态处理器
local function onPlayerStateChanged(player: Player, newPlayerState: string)
-- 爆破器状态只有当玩家状态为“游戏”时才是“准备”状态
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() 被频繁调用:例如在初始设置回合时,将自己设置在主代码路径上,玩家选择爆破器后,玩家返回大厅或 中立 生成地点。此外,玩家选择爆破器之后,服务器脚本服务 > 爆破器选择处理器 >将设置为 ,最后通过调用来移除力场。
自定义力场
而不是使用自定义实现,样例激光标签体验使用 Studio 的内置 ForceField 类来防止玩家在选择爆破器时受到伤害。这保证唯一要求玩家使用力场重生的要求是包含具有 SpawnLocation.Duration 属性的重生位置,该属性大于 0。该示例使用随机值 9,999 启用力场,然后在 复制存储 > 力场客户端视觉 中处理实际持续时间程序化。
与 setupHumanoidAsync 类似,ForceFieldClientVisuals 中的大多数线是可选的。例如,如果你像以下脚本一样评论函数的内容,体验将使用默认的闪光力场而不是六边形脚本在 StarterGui > ForceFieldGui 中。
在ForceFieldClientVisuals中评论属性
local function onCharacterAddedAsync(character: Model)
-- 本地力场 = 角色:等待孩子("力场", 3)
-- 如果不是 forceField 的话
-- 回传回
-- 结束
-- forceField.Visible=false
-- localPlayer.PlayerGui:WaitForChild("力场控制器")启用=真
-- forceField.Destroying:等待()
-- localPlayer.PlayerGui.ForceFieldGui.Enabled=false
end
因为自定义力场是图形用户界面而不是新的 ,所以 脚本只影响每个玩家的第一人称视觉效果,当玩家看到其他玩家时,第三人称视觉效果不受影响。第三人称视觉仍保留默认 Roblox 外观。了解有关修改力场的更多信息,请参阅 ForceField.Visible。


力场有用,因为它们为玩家提供足够的时间来进行生成和重生,而无需担心敌方玩家,但最终需要消失以进行主要的激光标签游戏玩法处理力场移除的脚本位于 复制存储 > 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 boolean。由于检查是按顺序运行的,脚本可以调用 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(`Invalid player state ({newPlayerState})`)
end
end
所有事件响应在此脚本中逻辑组合在一起,因为它们需要启用或禁用玩家控控制、相机移动和哪个 UI 层可见的相似行为。例如,在爆破器选择期间,玩家需要既不可移动工具动又不可攻击。服务器已经处理力场,但客户端处理移动。为了说明,如果您检查 onSelectingBlaster() 函数的逻辑,您可以看到客户端在他们选择爆破器时禁用玩家移动。
玩家状态处理器
local function onSelectingBlaster()
togglePlayerCamera(true)
togglePlayerMovement(false)
setGuiExclusivelyEnabled(playerGui.PickABlasterGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
end
onPlaying() 函数也是相同的简单。它启用移动,转到主显示头(HUD),启用爆破器,并调用与服务器相同的力场函数。
玩家状态处理器
local function onPlaying()
togglePlayerMovement(true)
setGuiExclusivelyEnabled(playerGui.HUDGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Ready)
scheduleDestroyForceField()
end
重生角色
样本激光标签体验处理重生角色回到 onTaggedOut() 状态通过 ReplicatedStorage > PlayerStateHandler 回合。像 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 -- new line, in seconds
杂项设置
作为初始设置的一部分,样品激光标签体验也执行了一些小但关键的步骤:
体验包括一个名为 StarterPlayer > StarterCharacterScripts > 生命值 的空脚本,可禁用默认 Roblox 生命值再生。要了解此属性的行为说明,请参阅 Humanoid.Health 。
体验使用第一人称相机通过设置 StarterPlayer.CameraMode.LockFirstPerson 属性来实现。请注意,如果你想让用户在第一人称和第三人称摄像头之间进行切换,你必须通过编程修改属性,而不是仅在 Studio 中设置一次,并修改控件和用户界面来补偿视角的变化。
现在玩家可以重生点、选择爆破器并从第一人称视查看瞄准它,下一节教你关于创建回合游戏的脚本背后的知识。