Spawn é o processo de criar um objeto ou personagem em uma experiência, e respawn é o processo de adicionar um objeto ou personagem de volta a uma experiência após atender a uma condição de remoção, como a saúde de um personagem chegando a zero ou caindo do mapa. Ambos os processos são importantes porque garantem que os jogadores possam entrar na sua experiência e continuar jogando para melhorar suas habilidades.
Usando a experiência de laser tag de amostra como referência, esta seção do tutorial ensina como usar e personalizar os recursos integrados do Roblox para lidar com spawn e respawn, incluindo orientação de script sobre:
- Configurar locais de spawn para que os jogadores possam apenas aparecer na zona de spawn de sua equipe.
- Adicionar novos jogadores e seus personagens à rodada conforme eles entram na experiência.
- Personalizar campos de força que previnem danos enquanto os jogadores aparecem e reaparecem.
- Lidar com o estado do cliente para que a jogabilidade funcione corretamente no momento apropriado.
- Reaparecer personagens após serem eliminados da rodada.
- Executar pequenas ações diversas que são cruciais para definir parâmetros de jogabilidade e características dos personagens.
Esta seção inclui bastante conteúdo sobre script, mas em vez de escrever tudo do zero ao criar uma experiência, ela incentiva você a aproveitar componentes existentes, iterar rapidamente e descobrir quais sistemas precisam de uma implementação personalizada para corresponder à sua visão. Após completar esta seção, você aprenderá como implementar jogabilidade baseada em rounds que rastreia pontos, monitora o estado do jogador e exibe os resultados da rodada.
Configurar locais de spawn
Se você fosse testar a experiência agora, todos os jogadores apareceriam aleatoriamente no objeto SpawnLocation na zona de spawn da equipe verde ou no objeto SpawnLocation na zona de spawn da equipe rosa. Isso apresenta um problema de jogabilidade, onde os jogadores poderiam marcar uns aos outros dentro de cada zona de spawn assim que o campo de força do oponente desaparecesse.
Para combater esse problema, a experiência de laser tag de amostra configura ambas as localizações de spawn com uma propriedade Neutral definida como false para restringir os jogadores da equipe oponente de aparecer na zona de spawn errada, e uma propriedade TeamColor configurada com o correspondente valor Team.Color da seção anterior do tutorial sobre Atribuição de Cores de Equipe:
- TeamASpawn – A localização de spawn na zona de spawn da equipe verde com uma propriedade TeamColor definida como Mint.
- TeamBSpawn – A localização de spawn na zona de spawn da equipe rosa com uma propriedade TeamColor definida como Carnation Pink.


Quando um jogador entra na experiência, ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ spawnPlayersInMap verifica quantos jogadores já estão em cada equipe e retorna a equipe com o menor número de jogadores.
spawnPlayersInMap
local function getSmallestTeam(): Team
local teams = Teams:GetTeams()
-- Ordena as equipes em ordem crescente do menor para o maior
table.sort(teams, function(teamA: Team, teamB: Team)
return #teamA:GetPlayers() < #teamB:GetPlayers()
end)
-- Retorna a menor equipe
return teams[1]
end
Uma vez que ele sabe qual é a equipe com o menor número de jogadores, ele classifica o jogador nessa equipe, define sua propriedade Player.Neutral como false para que o jogador possa apenas spawnar e respawnar em sua localização de spawn, e então define seu PlayerState para SelectingBlaster, sobre o qual você aprenderá mais adiante no tutorial.
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
Se você examinar Workspace ⟩ World ⟩ Map ⟩ Spawns, pode ver que há uma localização de spawn a mais no mapa: NeutralSpawn. Esta localização de spawn é única em relação às outras porque não tem a propriedade TeamColor definida para uma das duas equipes na experiência; em vez disso, esta localização de spawn possui uma propriedade Neutral que muda dependendo se uma rodada está ativa.
Por exemplo, se a rodada está ativa, a propriedade Neutral é definida como false para que spawnPlayersInMap possa classificar os jogadores em equipes e spawná-los na arena. No entanto, se a rodada não estiver ativa, como no tempo entre uma rodada e outra, a propriedade Neutral é definida como true para que os jogadores possam spawnar ali, independentemente de seu status de equipe. Esse processo é o que torna a localização de spawn Neutral um lobby funcional.

Para demonstrar, se você examinar ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ SpawnPlayersInLobby, que é executado no final de uma rodada, você pode ver que para cada jogador passado na tabela players: { Player }, o script:
- Define sua propriedade Player.Neutral como true para redefinir automaticamente seu Player.Team para nil, permitindo que o jogador respawn no lobby quando uma rodada não está ativa, já que a propriedade Neutral da localização de spawn também é definida como true.
- Muda seu PlayerState para InLobby para remover a blaster e os visuais da interface do usuário em primeira pessoa.
Para mais informações sobre a zona de spawn neutra e sua funcionalidade para cada rodada, veja Adicionando Rodadas na próxima seção do tutorial.
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
Conectar novos jogadores
O código Luau no Studio é muitas vezes orientado por eventos, o que significa que os scripts escutam eventos de um serviço do Roblox, e então chamam uma função em resposta. Por exemplo, ao adicionar novos jogadores a uma experiência multiplayer, deve haver um evento que lide com tudo o que é necessário para que os jogadores se conectem com sucesso. Na experiência de laser tag de amostra, esse evento correspondente é Players.PlayerAdded:Connect.
Players.PlayerAdded:Connect é parte de múltiplos scripts na experiência. Se você usar o atalho Ctrl/Cmd+Shift+F e pesquisar por Players.PlayerAdded:Connect, os resultados fornecem um bom ponto de partida para entender a configuração inicial da experiência.

Para demonstrar, abra ServerScriptService ⟩ SetupHumanoid. A distinção entre Player e Character é fundamental para entender este script:
- Os jogadores precisam escolher uma blaster e serem adicionados ao placar. Os personagens precisam spawnar e receber uma blaster.
SetupHumanoid imediatamente verifica se o jogador tem um personagem (acabou de entrar) ou não (está respawnando). Após encontrar um, ele chama onCharacterAdded(), obtém o modelo Humanoid do personagem e o envia para ServerScriptService ⟩ SetupHumanoid ⟩ setupHumanoidAsync para personalização. Depois de definir esses valores, o script espera que a saúde do personagem chegue a zero. Você aprenderá mais sobre respawn mais adiante nesta seção do tutorial.
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
A nota importante com este script é que as propriedades são completamente opcionais, o que significa que se você remover as primeiras seis linhas da função, a experiência ainda funcionará corretamente. Em vez de serem requisitos funcionais, cada propriedade permite que você tome decisões de design que atendam aos seus objetivos de jogabilidade. Por exemplo:
- Se você quiser que os nomes dos personagens sejam exibidos a distâncias mais curtas, reduza o valor de Humanoid.NameDisplayDistance.
- Se você quiser que a saúde de um personagem só seja exibida se estiver abaixo de 100%, defina Humanoid.HealthDisplayType como DisplayWhenDamaged.
- Se você quiser que os personagens se desintegrem quando sua saúde chegar a 0, defina Humanoid.BreakJointsOnDeath como True.
Se você alterar os valores dessas propriedades, é importante testar a experiência para que você possa ver o impacto das novas configurações. Você pode recriar o que os jogadores experimentam em uma simulação de multi-cliente selecionando pelo menos dois personagens em um teste de Server & Clients a partir da mezzanino.

Outro exemplo do evento Players.PlayerAdded:Connect está em ServerScriptService ⟩ PlayerStateHandler. Assim como no exemplo anterior, PlayerStateHandler imediatamente verifica a presença de um personagem. Se o jogador não estiver no lobby, o script define um atributo do jogador para o estado SelectingBlaster, o estado inicial para uma rodada em que os jogadores podem escolher entre dois tipos diferentes de blasters após aparecer na arena. Esse estado também inclui um campo de força que impede os jogadores de receberem dano enquanto fazem sua seleção.
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)
Uma variável específica em PlayerStateHandler merece discussão: attributeChangedConnectionByPlayer. Esta tabela armazena todos os jogadores e suas Connections para sinal de GetAttributeChangedSignal. A razão para armazenar essa conexão em uma tabela é que PlayerStateHandler pode desconectar essa conexão quando o jogador sai da experiência. Esse processo serve como uma forma de gerenciamento de memória para evitar que o número de conexões cresça continuamente ao longo do tempo.
PlayerStateHandler
local attributeChangedConnectionByPlayer = {}
local function onPlayerAdded(player: Player)
-- Lida com todas as futuras atualizações de estado do jogador
attributeChangedConnectionByPlayer[player] = player
:GetAttributeChangedSignal(PlayerAttribute.playerState)
:Connect(function()
local newPlayerState = player:GetAttribute(PlayerAttribute.playerState)
onPlayerStateChanged(player, newPlayerState)
end)
end
-- Desconecta a conexão de mudança de atributo quando o jogador sai
local function onPlayerRemoving(player: Player)
if attributeChangedConnectionByPlayer[player] then
attributeChangedConnectionByPlayer[player]:Disconnect()
attributeChangedConnectionByPlayer[player] = nil
end
end
Você pode ver que ambas as funções conectadas em onPlayerAdded() chamam onPlayerStateChanged(). Durante a configuração inicial, após um jogador ser classificado em uma equipe, onPlayerAdded() define PlayerState como SelectingBlaster, então a primeira instrução if avalia como falsa e desabilita o BlasterState. Na seção Implementar blasters do tutorial, você aprenderá mais detalhes sobre esse processo.
PlayerStateHandler
local function onPlayerStateChanged(player: Player, newPlayerState: string)
-- O estado do blaster é 'Ready' apenas se o estado do jogador for 'Playing'
local newBlasterState = if newPlayerState == PlayerState.Playing then BlasterState.Ready else BlasterState.Disabled
-- Agenda a lógica de destruição do campo de força quando o jogador começa a jogar
if newPlayerState == PlayerState.Playing then
scheduleDestroyForceField(player)
end
player:SetAttribute(PlayerAttribute.blasterStateServer, newBlasterState)
end
Se você adicionar pontos de interrupção ou mesmo uma instrução print(), pode ver que onPlayerStateChanged() é chamado com frequência ao longo da experiência: como durante a configuração inicial de uma rodada, para se definir no caminho de código principal, após o jogador escolher uma blaster e quando o jogador retorna ao lobby ou à localização de spawn Neutral. Além disso, após o jogador escolher uma blaster, ServerScriptService ⟩ BlasterSelectedHandler define o PlayerState como Playing, e PlayerStateHandler pode finalmente remover o campo de força chamando scheduleDestroyForceField().
Personalizar campos de força
Em vez de usar uma implementação personalizada, a experiência de laser tag de amostra usa a classe ForceField integrada do Studio para prevenir que os jogadores recebam danos enquanto escolhem sua blaster. Isso garante que os únicos requisitos para os jogadores spawnarem com um campo de força sejam incluir localizações de spawn com uma propriedade SpawnLocation.Duration que seja maior que 0. A amostra usa um valor arbitrário de 9,999 para habilitar campos de força, depois manipula a duração real programaticamente em ReplicatedStorage ⟩ ForceFieldClientVisuals.
Assim como setupHumanoidAsync, a maioria das linhas em ForceFieldClientVisuals são opcionais. Por exemplo, se você comentar o conteúdo da função como o seguinte script faz, a experiência utilizará o campo de força padrão cintilante em vez do script hexagonal em StarterGui ⟩ ForceFieldGui.
Comentando Propriedades em 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
Como o campo de força personalizado é uma interface gráfica em vez de um novo ParticleEmitter, o script ForceFieldClientVisuals afeta apenas os visuais em primeira pessoa para cada jogador, não os visuais em terceira pessoa quando os jogadores olham para outros jogadores. Os visuais em terceira pessoa mantêm a aparência padrão do Roblox. Para mais informações sobre como modificar os campos de força, consulte ForceField.Visible.


Os campos de força são úteis porque fornecem tempo suficiente para os jogadores entre spawn e respawn sem precisar se preocupar com jogadores inimigos, mas eventualmente precisam desaparecer para a principal jogabilidade de laser tag. O script que lida com a remoção do campo de força está em ReplicatedStorage ⟩ scheduleDestroyForceField, e verifica três condições únicas:
- Após os jogadores escolherem uma blaster, os campos de força precisam durar o suficiente para permitir que os jogadores se acclimatem ao seu entorno.
- Durante esse tempo de acclimatação, os campos de força não podem ser uma vantagem, portanto, precisam desaparecer no momento em que um jogador aciona sua blaster.
- Os campos de força precisam desaparecer quando os jogadores redefinirem seus personagens, seja antes de disparar ou antes de o campo de força expirar.
Cada uma dessas verificações no script scheduleDestroyForceField chama endForceField() para essas condições.
scheduleDestroyForceField
-- Finaliza o campo de força se o jogador disparar
local blasterStateAttribute = getBlasterStateAttribute()
attributeChangedConnection = player:GetAttributeChangedSignal(blasterStateAttribute):Connect(function()
local currentBlasterState = player:GetAttribute(blasterStateAttribute)
if currentBlasterState == BlasterState.Blasting then
endForceField()
end
end)
-- Finaliza o campo de força se o jogador redefinir
characterRespawnedConnection = player.CharacterRemoving:Connect(endForceField)
-- Finaliza o campo de força após 8 segundos
task.delay(MAX_FORCE_FIELD_TIME, endForceField)
endForceField() inclui uma declaração if aparentemente estranha em torno do booleano forceFieldEnded. Como as verificações são executadas sequencialmente, o script pode chamar a função endForceField() duas ou até três vezes. O booleano forceFieldEnded garante que a função tente destruir um campo de força apenas uma vez.
scheduleDestroyForceField
local function endForceField()
if forceFieldEnded then
return
end
forceFieldEnded = true
attributeChangedConnection:Disconnect()
characterRespawnedConnection:Disconnect()
destroyForceField(player)
end
Lidar com o estado do cliente
Enquanto a maior parte desta seção foca em ServerScriptService ⟩ PlayerStateHandler, há outro script com o mesmo nome em ReplicatedStorage. A razão para essa divisão é a arquitetura cliente-servidor:
O cliente precisa entender informações de estado do jogador para poder responder adequadamente em tempo real, como exibindo os elementos corretos da interface do usuário, ou permitindo que os jogadores se movimentem e disparem.
O servidor precisa de todas essas mesmas informações para poder prevenir exploits. Por exemplo, o servidor também precisa do estado do jogador para realizar ações como spawn e equipar personagens, desabilitar campos de força e exibir um placar. É por isso que esse script está em ReplicatedStorage e não em um local puramente do lado do cliente.
Para ver essa lógica central, revise o seguinte script em ReplicatedStorage ⟩ PlayerStateHandler que verifica o estado atual do usuário, e então chama a função apropriada que lida com as ações correspondentes para aquele estado.
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(`Estado de jogador inválido ({newPlayerState})`)
end
end
Todas as respostas a eventos são logicamente agrupadas neste script porque requerem comportamentos similares de habilitar ou desabilitar controles dos jogadores, movimentação da câmera e qual camada da interface do usuário está visível. Por exemplo, durante a seleção da blaster, os jogadores precisam ser invulneráveis e incapazes de se mover. O servidor já lida com o campo de força, mas o cliente lida com a movimentação. Para demonstrar, se você verificar a lógica da função onSelectingBlaster(), pode ver que o cliente desabilita a movimentação do jogador enquanto eles estão escolhendo uma blaster.
PlayerStateHandler
local function onSelectingBlaster()
togglePlayerCamera(true)
togglePlayerMovement(false)
setGuiExclusivelyEnabled(playerGui.PickABlasterGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
end
A função onPlaying() é igualmente direta. Ela habilita a movimentação, faz a transição para a interface principal (HUD), habilita a blaster e chama a mesma função de campo de força que o servidor.
PlayerStateHandler
local function onPlaying()
togglePlayerMovement(true)
setGuiExclusivelyEnabled(playerGui.HUDGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Ready)
scheduleDestroyForceField()
end
Respawn de personagens
A experiência de laser tag de amostra lida com o respawn de personagens de volta a uma rodada através do estado onTaggedOut() em ReplicatedStorage ⟩ PlayerStateHandler. Assim como os estados onSelectingBlaster() e onPlaying(), onTaggedOut() aciona comportamentos únicos de acordo com as mudanças no atributo playerState. Especificamente, ele desabilita a movimentação do jogador, apresenta a interface de respawn e desabilita a blaster.
PlayerStateHandler
local function onTaggedOut()
-- Desabilita os controles enquanto o jogador está marcado
togglePlayerMovement(false)
togglePlayerCamera(false)
setGuiExclusivelyEnabled(playerGui.OutStateGui)
-- Desabilita a blaster enquanto o jogador está marcado
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
end
Se você quiser testar esse comportamento, pode pressionar Esc, navegar até a aba Configurações, e clicar no botão Redefinir Personagem. Note que quando você aciona a tela de respawn, não pode se mover, rotacionar a câmera ou disparar sua blaster.


É importante notar que este script não respawn realmente os personagens, ele apenas impede que eles atuem e fornece feedback visual aos jogadores de que o servidor está respawnando seus personagens. Para demonstrar, se você examinar ServerScriptService ⟩ SetupHumanoid ⟩ setupHumanoidAsync ⟩ onHumanoidDied, o script define PlayerState como TaggedOut (notificando essencialmente ReplicatedStorage ⟩ PlayerStateHandler), e adiciona alguns indicadores visuais. A lógica real de respawn é um comportamento integrado ao Roblox.
Quando os jogadores respawnam de volta na rodada, eles reaparecem na localização de spawn de sua equipe de acordo com a propriedade SpawnLocation.TeamColor. Para personalizar o tempo de respawn, você pode adicionar a seguinte linha ao topo de SetupHumanoid. Para aprender mais sobre essa técnica, consulte Players.RespawnTime.
SetupHumanoidlocal Players = game:GetService("Players")Players.RespawnTime = 10 -- nova linha, em segundos
Configurações diversas
Como parte da configuração inicial, a experiência de laser tag de amostra também realiza alguns pequenos, mas críticos passos:
A experiência inclui um script vazio chamado StarterPlayer ⟩ StarterCharacterScripts ⟩ Health que desabilita a regeneração de saúde padrão do Roblox. Para uma explicação do comportamento dessa propriedade, veja Humanoid.Health.
A experiência usa uma câmera em primeira pessoa definindo a propriedade StarterPlayer.CameraMode.LockFirstPerson. Note que se você quiser permitir que os usuários mudem entre câmeras de primeira e terceira pessoa, você deve alterar a propriedade programaticamente em vez de apenas defini-la uma vez no Studio e modificar os controles e a interface do usuário para compensar a mudança de perspectiva.
A experiência utiliza o placar integrado do Roblox com a unidade de "pontos", que os jogadores ganham cada vez que marcam outro jogador. Você pode ver a configuração em ServerScriptService ⟩ SetupLeaderboard, mas Placar em Experiência oferece uma visão completa. Note que onPlayerTagged adiciona pontos ao placar, sobre o qual você aprenderá em Adicionar Rodadas e Detectar Acertos.
Agora que os jogadores podem spawnar, escolher uma blaster e mira a partir de um ponto de vista em primeira pessoa, a próxima seção ensina sobre os scripts por trás da criação de jogabilidade baseada em rodadas.