生成和重生

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处

生成是指在游戏中创建对象或角色的过程,而重生是指在满足移除条件后将对象或角色重新添加回游戏的过程,例如角色的生命值降至零或掉出地图。这两个过程都很重要,因为它们确保玩家能够加入你的游戏,并可以继续玩以提高他们的技能。

示例激光标记游戏为参考,本教程的这一部分教你如何使用和自定义Roblox的内置功能来处理生成和重生,包括脚本指导:

  • 配置生成位置,以便玩家只能在他们团队的生成区生成。
  • 在玩家加入游戏时将新玩家及其角色添加到回合中。
  • 自定义防止伤害的力场,以便玩家在生成和重生时使用。
  • 处理客户端状态,以便在适当的时间正确进行游戏。
  • 在角色被标记出局后重生角色。
  • 执行一些小的杂项操作,这些操作对于设置游戏玩法和角色参数至关重要。

本节包含大量脚本内容,但在创建游戏时,它鼓励你利用现有组件,快速迭代,并找出哪些系统需要自定义实现以匹配你的愿景。完成本节后,你将学习如何实现基于回合的游戏玩法,跟踪积分,监控玩家状态,并显示回合结果。

配置生成位置

如果你现在进行游戏测试,所有玩家将随机在绿色团队的生成区的SpawnLocation对象或粉色团队的生成区的SpawnLocation对象生成。这会导致一个游戏问题,即玩家可以在对手的力场消失后立即在各自的生成区互相标记。

为了解决这个问题,示例激光标记游戏将两个生成位置的Neutral属性设置为false,以限制对方团队的玩家在错误的生成区生成,并将TeamColor属性设置为来自分配团队颜色的相应Team.Color值:

  • TeamASpawn – 绿色团队生成区的生成位置,其TeamColor属性设置为薄荷绿
  • TeamBSpawn – 粉色团队生成区的生成位置,其TeamColor属性设置为康乃馨粉
TeamASpawn
TeamBSpawn

当玩家加入游戏时,ServerScriptServiceGameplayRoundsspawnPlayersInMap检查每个团队中已经有多少玩家,然后返回玩家最少的团队。

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

如果你检查WorkspaceWorldMapSpawns,你会看到地图中还有一个生成位置:NeutralSpawn。这个生成位置与其他位置不同,因为它没有将TeamColor属性设置为游戏中的两个团队之一;相反,这个生成位置的Neutral属性会根据回合是否处于活动状态而变化。

例如,如果回合处于活动状态,Neutral属性设置为false,以便spawnPlayersInMap可以将玩家分配到团队并将他们生成到竞技场中。然而,如果回合不处于活动状态,例如在一个回合和下一个回合之间的时间,Neutral属性设置为true,以便玩家可以在此处生成,而不管他们的团队状态。这一过程使得Neutral生成位置成为一个功能性大厅。

Neutral

为了演示,如果你检查ServerScriptServiceGameplayRoundsSpawnPlayersInLobby,该脚本在回合结束时运行,你会看到对于传递到players: { Player }表中的每个玩家,脚本:

  • 将他们的Player.Neutral属性设置为true,以自动重置他们的Player.Teamnil,允许玩家在回合不活跃时在大厅中重生,因为生成位置的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,结果为理解游戏的初始设置提供了一个良好的起点。

Studio的查找所有窗口,突出显示Players.PlayerAdded结果。

为了演示,打开ServerScriptServiceSetupHumanoidPlayerCharacter之间的区别是理解此脚本的关键:

  • 玩家是一个连接的客户端,而角色是一个Humanoid模型。
  • 玩家需要选择一个标枪并被添加到排行榜中。角色需要生成并获得一个标枪。

SetupHumanoid立即检查玩家是否有角色(刚加入)或没有(正在重生)。在找到一个角色后,它调用onCharacterAdded(),从角色中获取Humanoid模型,并将其传递给ServerScriptServiceSetupHumanoidsetupHumanoidAsync进行自定义。在设置这些值后,脚本等待角色的生命值降至零。你将在本节后面了解更多关于重生的内容。

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

此脚本的重要说明是这些属性都是完全可选的,这意味着如果你删除函数的前六行,游戏仍然可以正常工作。每个属性允许你做出符合游戏玩法目标的设计决策,而不是功能要求。例如:

如果你更改这些属性的值,进行游戏测试是很重要的,这样你可以看到新设置的影响。你可以通过在Server & Clients的游戏测试中选择至少两个角色来重现玩家在多客户端模拟中的体验。

Studio的夹层中测试模式下的服务器和客户端选项。

Players.PlayerAdded:Connect事件的另一个示例在ServerScriptServicePlayerStateHandler中。与前面的示例一样,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。这个表存储所有玩家及其ConnectionsGetAttributeChangedSignal。将此连接存储在表中的原因是,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生成位置时。此外,在玩家选择标枪后,ServerScriptServiceBlasterSelectedHandlerPlayerState设置为Playing,而PlayerStateHandler最终可以通过调用scheduleDestroyForceField()来移除力场。

自定义力场

示例激光标记游戏使用Studio的内置ForceField类来防止玩家在选择标枪时受到伤害,而不是使用自定义实现。这确保了玩家生成时只需包含SpawnLocation.Duration属性大于0的生成位置即可启用力场。示例使用了一个任意值9,999来启用力场,然后在ReplicatedStorageForceFieldClientVisuals中以编程方式处理实际持续时间。

setupHumanoidAsync类似,ForceFieldClientVisuals中的大多数行都是可选的。例如,如果你像以下脚本那样注释掉函数的内容,游戏将使用默认的闪烁力场,而不是StarterGuiForceFieldGui中的六边形脚本。

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,而不是新的ParticleEmitterForceFieldClientVisuals脚本仅影响每个玩家的第一人称视觉效果,而影响玩家在查看其他玩家时的第三人称视觉效果。第三人称视觉效果保留了默认的Roblox外观。有关修改力场的更多信息,请参见ForceField.Visible

第一人称力场视觉效果包括屏幕周围的未来派六边形网格。
第一人称力场视觉效果
第三人称力场视觉效果包括在玩家生成到游戏时围绕玩家的蓝色闪烁球体。
第三人称力场视觉效果

力场很有用,因为它们为玩家在生成和重生之间提供了足够的时间,而无需担心敌方玩家,但最终它们需要消失以进行主要的激光标记游戏。处理力场移除的脚本位于ReplicatedStoragescheduleDestroyForceField,它检查三个独特的条件:

  • 玩家选择标枪后,力场需要持续足够长的时间,以便玩家适应他们的环境。
  • 在此适应时间内,力场不能成为优势,因此它们需要在玩家发射标枪的瞬间消失。
  • 当玩家重置角色时,无论是在发射之前还是在力场超时之前,力场都需要消失。

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

处理客户端状态

虽然本节的大部分内容集中在ServerScriptServicePlayerStateHandler上,但在ReplicatedStorage中还有一个同名的脚本。拆分的原因是客户端-服务器架构:

  • 客户端需要理解玩家状态信息,以便能够实时做出适当的响应,例如显示正确的用户界面元素,或使玩家能够移动和发射。

  • 服务器需要所有这些相同的信息,以便能够防止利用。例如,服务器还需要玩家状态来执行诸如生成和装备角色、禁用力场和显示排行榜等操作。这就是为什么这个脚本在ReplicatedStorage中,而不是纯粹的客户端位置。

要查看这个核心逻辑,请查看ReplicatedStoragePlayerStateHandler中的以下脚本,该脚本验证用户的当前状态,然后调用处理该状态相应操作的适当函数。

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

重生角色

示例激光标记游戏通过ReplicatedStoragePlayerStateHandler中的onTaggedOut()状态处理角色重生。与onSelectingBlaster()onPlaying()状态一样,onTaggedOut()根据playerState属性的变化触发独特的行为。具体来说,它禁用玩家移动,呈现重生用户界面,并禁用标枪。

PlayerStateHandler
local function onTaggedOut()
-- 被标记出局时禁用控制
togglePlayerMovement(false)
togglePlayerCamera(false)
setGuiExclusivelyEnabled(playerGui.OutStateGui)
-- 被标记出局时禁用标枪
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
end

如果你想测试这种行为,可以按Esc,导航到设置选项卡,然后单击重置角色按钮。注意,当你触发重生屏幕时,你无法移动、旋转相机或发射标枪。

Roblox的设置菜单,突出显示重置角色按钮。
重置角色按钮
重生屏幕在玩家重生回合时显示。
重生屏幕

重要的是要注意,这个脚本实际上并不重生角色,它只是阻止他们行动并向玩家提供视觉反馈,表明服务器正在重生他们的角色。为了演示,如果你检查ServerScriptServiceSetupHumanoidsetupHumanoidAsynconHumanoidDied,脚本将PlayerState设置为TaggedOut(本质上通知ReplicatedStoragePlayerStateHandler),并添加一些视觉指示器。重生的实际逻辑是Roblox的内置行为。

当玩家重生回合时,他们会根据SpawnLocation.TeamColor属性在他们团队的生成位置重生。要自定义重生时间,你可以在SetupHumanoid的顶部添加以下行。要了解更多关于此技术的信息,请参见Players.RespawnTime

SetupHumanoid
local Players = game:GetService("Players")
Players.RespawnTime = 10 -- 新行,以秒为单位

杂项设置

作为初始设置的一部分,示例激光标记游戏还执行一些小但关键的步骤:

  • 游戏包括一个名为StarterPlayerStarterCharacterScriptsHealth的空脚本,该脚本禁用默认的Roblox生命值再生。有关此属性行为的解释,请参见Humanoid.Health

  • 游戏通过设置StarterPlayer.CameraMode.LockFirstPerson属性使用第一人称相机。请注意,如果你想让用户在第一人称和第三人称相机之间切换,必须以编程方式更改该属性,而不仅仅是在Studio中设置一次,并修改控件和用户界面以补偿视角的变化。

  • 游戏使用内置的Roblox排行榜,单位为“积分”,玩家每次标记另一名玩家出局时都会获得积分。你可以在ServerScriptServiceSetupLeaderboard中查看配置,但游戏内排行榜提供了完整的概述。请注意,onPlayerTagged会向排行榜添加积分,你将在添加回合检测击中中了解。

现在玩家可以生成、选择标枪并从第一人称视角瞄准,下一节将教你关于创建基于回合的游戏玩法的脚本。

©2026 Roblox Corporation、Roblox、Roblox 标志及 Powering Imagination 是我们在美国及其他国家或地区的注册与未注册商标。