Generar es el proceso de crear un objeto o personaje en un juego, y regenerar es el proceso de añadir un objeto o personaje de nuevo en un juego después de que cumpla una condición de eliminación, como que la salud de un personaje llegue a cero o caiga del mapa. Ambos procesos son importantes porque aseguran que los jugadores puedan unirse a tu juego y continuar jugando para mejorar sus habilidades.
Usando el juego de láser tag de muestra como referencia, esta sección del tutorial te enseña cómo usar y personalizar las características integradas de Roblox para manejar la generación y regeneración, incluyendo orientación sobre scripting en:
- Configurar ubicaciones de generación para que los jugadores solo puedan generar en la zona de generación de su equipo.
- Añadir nuevos jugadores y su personaje a la ronda a medida que se unen al juego.
- Personalizar campos de fuerza que previenen daño mientras los jugadores generan y regeneran.
- Manejar el estado del cliente para que el juego funcione correctamente en el momento adecuado.
- Regenerar personajes después de que sean eliminados de la ronda.
- Realizar pequeñas acciones diversas que son cruciales para establecer los parámetros del juego y del personaje.
Esta sección incluye mucho contenido de scripting, pero en lugar de escribir todo desde cero al crear un juego, te anima a aprovechar los componentes existentes, iterar rápidamente y averiguar qué sistemas necesitan una implementación personalizada para coincidir con tu visión. Después de completar esta sección, aprenderás cómo implementar un juego basado en rondas que rastrea puntos, monitorea el estado del jugador y muestra los resultados de la ronda.
Configurar ubicaciones de generación
Si probaras el juego en este momento, todos los jugadores aparecerían aleatoriamente en el objeto SpawnLocation en la zona de generación del equipo verde, o en el objeto SpawnLocation en la zona de generación del equipo rosa. Esto presenta un problema de jugabilidad donde los jugadores podrían etiquetarse entre sí dentro de cada zona de generación tan pronto como el campo de fuerza de su oponente desaparezca.
Para combatir este problema, el juego de láser tag de muestra configura ambas ubicaciones de generación con una propiedad Neutral establecida en false para restringir a los jugadores del equipo contrario de generar en la zona de generación incorrecta, y una propiedad TeamColor establecida en el valor correspondiente de Team.Color de Asignar colores de equipo en la sección anterior del tutorial:
- TeamASpawn – La ubicación de generación en la zona de generación del equipo verde con una propiedad TeamColor establecida en Mint.
- TeamBSpawn – La ubicación de generación en la zona de generación del equipo rosa con una propiedad TeamColor establecida en Carnation Pink.


Cuando un jugador se une al juego, ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ spawnPlayersInMap verifica cuántos jugadores ya están en cada equipo, y luego devuelve el equipo con la menor cantidad de jugadores.
local function getSmallestTeam(): Team
local teams = Teams:GetTeams()
-- Ordenar equipos en orden ascendente de menor a mayor
table.sort(teams, function(teamA: Team, teamB: Team)
return #teamA:GetPlayers() < #teamB:GetPlayers()
end)
-- Devolver el equipo más pequeño
return teams[1]
endUna vez que sabe cuál es el equipo con la menor cantidad de jugadores, clasifica al jugador en ese equipo, establece su propiedad Player.Neutral en false para que el jugador solo pueda generar y regenerar en la ubicación de generación de su equipo, y luego establece su PlayerState en SelectingBlaster, que aprenderás más adelante en el tutorial.
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
endSi examinas Workspace ⟩ World ⟩ Map ⟩ Spawns, puedes ver que hay una ubicación de generación más en el mapa: NeutralSpawn. Esta ubicación de generación es única en comparación con las otras porque no tiene una propiedad TeamColor establecida en uno de los dos equipos en el juego; en cambio, esta ubicación de generación tiene una propiedad Neutral que cambia dependiendo de si una ronda está activa.
Por ejemplo, si la ronda está activa, la propiedad Neutral se establece en false para que spawnPlayersInMap pueda clasificar a los jugadores en equipos y generarlos en la arena. Sin embargo, si la ronda no está activa, como el tiempo entre una ronda y la siguiente, la propiedad Neutral se establece en true para que los jugadores puedan generar allí independientemente de su estado de equipo. Este proceso es lo que hace que la ubicación de generación Neutral sea un vestíbulo funcional.

Para demostrar, si examinas ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ SpawnPlayersInLobby, que se ejecuta al final de una ronda, puedes ver que para cada jugador que se pasa a la tabla players: { Player }, el script:
- Establece su propiedad Player.Neutral en true para restablecer automáticamente su Player.Team a nil, permitiendo que el jugador regenere en el vestíbulo cuando una ronda no está activa, ya que la propiedad Neutral de la ubicación de generación también se establece en true.
- Cambia su PlayerState a InLobby para eliminar el blaster del jugador y los elementos visuales de la interfaz de usuario en primera persona.
Para más información sobre la zona de generación neutral y su funcionalidad para cada ronda, consulta Añadiendo rondas en la siguiente sección del tutorial.
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
endConectar nuevos jugadores
El código Luau en Studio a menudo está impulsado por eventos, lo que significa que los scripts escuchan eventos de un servicio de Roblox y luego llaman a una función en respuesta. Por ejemplo, al añadir nuevos jugadores a un juego multijugador, debe haber un evento que maneje todo lo necesario para que los jugadores se conecten con éxito. En el juego de láser tag de muestra, este evento correspondiente es Players.PlayerAdded:Connect.
Players.PlayerAdded:Connect es parte de múltiples scripts en el juego. Si usas el atajo Ctrl/Cmd+Shift+F y buscas Players.PlayerAdded:Connect, los resultados proporcionan un buen punto de partida para entender la configuración inicial del juego.

Para demostrar, abre ServerScriptService ⟩ SetupHumanoid. La distinción entre Player y Character es clave para entender este script:
- Los jugadores necesitan elegir un blaster y ser añadidos a la tabla de clasificación. Los personajes necesitan generarse y recibir un blaster.
SetupHumanoid verifica inmediatamente si el jugador tiene un personaje (acaba de unirse) o no (está regenerando). Después de encontrar uno, llama a onCharacterAdded(), obtiene el modelo de Humanoid del personaje y lo pasa a ServerScriptService ⟩ SetupHumanoid ⟩ setupHumanoidAsync para personalización. Después de establecer estos valores, el script espera a que la salud del personaje llegue a cero. Aprenderás más sobre la regeneración más adelante en esta sección del tutorial.
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)
endLa nota importante con este script es que las propiedades son completamente opcionales, lo que significa que si eliminas las primeras seis líneas de la función, el juego seguirá funcionando correctamente. En lugar de ser requisitos funcionales, cada propiedad te permite tomar decisiones de diseño que cumplen con tus objetivos de jugabilidad. Por ejemplo:
- Si deseas que los nombres de los personajes se muestren a distancias más cercanas, reduce el valor de Humanoid.NameDisplayDistance.
- Si solo deseas que la salud de un personaje se muestre si está por debajo del 100%, establece Humanoid.HealthDisplayType en DisplayWhenDamaged.
- Si deseas que los personajes se descompongan cuando su salud llegue a 0, establece Humanoid.BreakJointsOnDeath en True.
Si cambias los valores de estas propiedades, es importante probar el juego para que puedas ver el impacto de tus nuevas configuraciones. Puedes recrear lo que los jugadores experimentan en una simulación de múltiples clientes seleccionando al menos dos personajes en una prueba de juego de Server & Clients desde el entrepiso.

Otro ejemplo del evento Players.PlayerAdded:Connect se encuentra en ServerScriptService ⟩ PlayerStateHandler. Al igual que en el ejemplo anterior, PlayerStateHandler verifica inmediatamente si hay un personaje. Si el jugador no está en el vestíbulo, el script establece un atributo del jugador en el estado SelectingBlaster, el estado inicial para una ronda en la que los jugadores pueden seleccionar entre dos tipos diferentes de blaster después de generarse en la arena. Este estado también incluye un campo de fuerza que previene que los jugadores reciban daño mientras hacen su selección.
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)Una variable particular en PlayerStateHandler merece discusión: attributeChangedConnectionByPlayer. Esta tabla almacena todos los jugadores y sus Connections al GetAttributeChangedSignal. La razón para almacenar esta conexión en una tabla es para que PlayerStateHandler pueda desconectarla cuando el jugador abandona el juego. Este proceso sirve como una especie de gestión de memoria para evitar que el número de conexiones crezca cada vez más con el tiempo.
local attributeChangedConnectionByPlayer = {}
local function onPlayerAdded(player: Player)
-- Manejar todas las futuras actualizaciones del estado del jugador
attributeChangedConnectionByPlayer[player] = player
:GetAttributeChangedSignal(PlayerAttribute.playerState)
:Connect(function()
local newPlayerState = player:GetAttribute(PlayerAttribute.playerState)
onPlayerStateChanged(player, newPlayerState)
end)
end
-- Desconectar de la conexión de atributo cambiado cuando el jugador se va
local function onPlayerRemoving(player: Player)
if attributeChangedConnectionByPlayer[player] then
attributeChangedConnectionByPlayer[player]:Disconnect()
attributeChangedConnectionByPlayer[player] = nil
end
endPuedes ver que ambas funciones conectadas en onPlayerAdded() llaman a onPlayerStateChanged(). Durante la configuración inicial después de que un jugador se clasifica en un equipo, onPlayerAdded() establece PlayerState en SelectingBlaster, por lo que la primera declaración if evalúa como falsa y desactiva el BlasterState. En la sección posterior Implementar blasters del tutorial, aprenderás más detalles sobre este proceso.
local function onPlayerStateChanged(player: Player, newPlayerState: string)
-- El estado del blaster es 'Listo' solo si el estado del jugador es 'Jugando'
local newBlasterState = if newPlayerState == PlayerState.Playing then BlasterState.Ready else BlasterState.Disabled
-- Programar la lógica de destrucción del campo de fuerza cuando el jugador comience a jugar
if newPlayerState == PlayerState.Playing then
scheduleDestroyForceField(player)
end
player:SetAttribute(PlayerAttribute.blasterStateServer, newBlasterState)
endSi añades puntos de interrupción o incluso solo una declaración print(), puedes ver que onPlayerStateChanged() se llama con frecuencia a lo largo del juego: como durante la configuración inicial de una ronda, para establecerse en la ruta de código principal, después de que el jugador elige un blaster, y cuando el jugador regresa al vestíbulo, o a la ubicación de generación Neutral. Además, después de que el jugador elige un blaster, ServerScriptService ⟩ BlasterSelectedHandler establece el PlayerState en Playing, y PlayerStateHandler finalmente puede eliminar el campo de fuerza llamando a scheduleDestroyForceField().
Personalizar campos de fuerza
En lugar de usar una implementación personalizada, el juego de láser tag de muestra utiliza la clase ForceField integrada de Studio para prevenir que los jugadores reciban daño mientras seleccionan su blaster. Esto asegura que el único requisito para que los jugadores generen con un campo de fuerza sea incluir ubicaciones de generación con una propiedad SpawnLocation.Duration que sea mayor que 0. La muestra utiliza un valor arbitrario de 9,999 para habilitar los campos de fuerza, y luego maneja la duración real programáticamente en ReplicatedStorage ⟩ ForceFieldClientVisuals.
Similar a setupHumanoidAsync, la mayoría de las líneas en ForceFieldClientVisuals son opcionales. Por ejemplo, si comentas el contenido de la función como lo hace el siguiente script, el juego utiliza el campo de fuerza brillante predeterminado en lugar del script hexagonal en StarterGui ⟩ ForceFieldGui.
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
endDebido a que el campo de fuerza personalizado es una GUI en lugar de un nuevo ParticleEmitter, el script ForceFieldClientVisuals solo afecta las visuales en primera persona para cada jugador, no las visuales en tercera persona cuando los jugadores miran a otros jugadores. Las visuales en tercera persona mantienen la apariencia predeterminada de Roblox. Para más información sobre cómo modificar los campos de fuerza, consulta ForceField.Visible.


Los campos de fuerza son útiles porque proporcionan a los jugadores suficiente tiempo entre la generación y la regeneración sin necesidad de preocuparse por los jugadores enemigos, pero eventualmente necesitan desaparecer para la jugabilidad principal del láser tag. El script que maneja la eliminación del campo de fuerza se encuentra en ReplicatedStorage ⟩ scheduleDestroyForceField, y verifica tres condiciones únicas:
- Después de que los jugadores seleccionan un blaster, los campos de fuerza necesitan durar lo suficiente para permitir que los jugadores se aclimaten a su entorno.
- Durante este tiempo de aclimatación, los campos de fuerza no pueden ser una ventaja, por lo que necesitan desaparecer en el momento en que un jugador dispare su blaster.
- Los campos de fuerza necesitan desaparecer cuando los jugadores reinician sus personajes, ya sea antes de disparar o antes de que el campo de fuerza se agote.
Cada una de estas verificaciones en el script scheduleDestroyForceField llama a endForceField() para estas condiciones.
-- Terminar el campo de fuerza si el jugador dispara
local blasterStateAttribute = getBlasterStateAttribute()
attributeChangedConnection = player:GetAttributeChangedSignal(blasterStateAttribute):Connect(function()
local currentBlasterState = player:GetAttribute(blasterStateAttribute)
if currentBlasterState == BlasterState.Blasting then
endForceField()
end
end)
-- Terminar el campo de fuerza si el jugador reinicia
characterRespawnedConnection = player.CharacterRemoving:Connect(endForceField)
-- Terminar el campo de fuerza después de 8 segundos
task.delay(MAX_FORCE_FIELD_TIME, endForceField)endForceField() incluye una declaración if aparentemente extraña alrededor del booleano forceFieldEnded. Dado que las verificaciones se ejecutan secuencialmente, el script puede llamar a la función endForceField() dos o incluso tres veces. El booleano forceFieldEnded asegura que la función solo intente destruir un campo de fuerza una vez.
local function endForceField()
if forceFieldEnded then
return
end
forceFieldEnded = true
attributeChangedConnection:Disconnect()
characterRespawnedConnection:Disconnect()
destroyForceField(player)
endManejar el estado del cliente
Mientras que la mayor parte de esta sección se centra en ServerScriptService ⟩ PlayerStateHandler, hay otro script con el mismo nombre en ReplicatedStorage. La razón de la división es la arquitectura cliente-servidor:
El cliente necesita entender la información del estado del jugador para que pueda responder apropiadamente en tiempo real, como mostrar los elementos de interfaz de usuario correctos, o permitir que los jugadores se muevan y disparen.
El servidor necesita toda esta misma información para que pueda prevenir exploits. Por ejemplo, el servidor también necesita el estado del jugador para realizar acciones como generar y equipar personajes, desactivar campos de fuerza y mostrar una tabla de clasificación. Por eso este script está en ReplicatedStorage y no en una ubicación puramente del lado del cliente.
Para ver esta lógica central, revisa el siguiente script en ReplicatedStorage ⟩ PlayerStateHandler que verifica el estado actual del usuario, y luego llama a la función apropiada que maneja las acciones correspondientes para ese estado.
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 jugador inválido ({newPlayerState})`)
end
endTodas las respuestas a eventos están lógicamente agrupadas en este script porque requieren un comportamiento similar de habilitar o deshabilitar los controles del jugador, el movimiento de la cámara y qué capa de UI es visible. Por ejemplo, durante la selección del blaster, los jugadores necesitan ser tanto invulnerables como incapaces de moverse. El servidor ya maneja el campo de fuerza, pero el cliente maneja el movimiento. Para demostrar, si revisas la lógica de la función onSelectingBlaster(), puedes ver que el cliente desactiva el movimiento del jugador mientras selecciona un blaster.
local function onSelectingBlaster()
togglePlayerCamera(true)
togglePlayerMovement(false)
setGuiExclusivelyEnabled(playerGui.PickABlasterGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
endLa función onPlaying() es igualmente sencilla. Habilita el movimiento, transiciona a la pantalla principal de visualización (HUD), habilita el blaster y llama a la misma función de campo de fuerza que el servidor.
local function onPlaying()
togglePlayerMovement(true)
setGuiExclusivelyEnabled(playerGui.HUDGui)
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Ready)
scheduleDestroyForceField()
endRegenerar personajes
El juego de láser tag de muestra maneja la regeneración de personajes de vuelta a una ronda a través del estado onTaggedOut() en ReplicatedStorage ⟩ PlayerStateHandler. Al igual que el estado onSelectingBlaster() y onPlaying(), onTaggedOut() desencadena un comportamiento único de acuerdo con los cambios en el atributo playerState. Específicamente, desactiva el movimiento del jugador, presenta la interfaz de usuario de regeneración y desactiva el blaster.
local function onTaggedOut()
-- Desactivar controles mientras está etiquetado
togglePlayerMovement(false)
togglePlayerCamera(false)
setGuiExclusivelyEnabled(playerGui.OutStateGui)
-- Desactivar blaster mientras está etiquetado
localPlayer:SetAttribute(PlayerAttribute.blasterStateClient, BlasterState.Disabled)
endSi deseas probar este comportamiento, puedes presionar Esc, navegar a la pestaña Configuración, y luego hacer clic en el botón Reiniciar personaje. Observa que cuando activas la pantalla de regeneración, no puedes moverte, rotar la cámara o disparar tu blaster.


Es importante notar que este script no respawnea realmente a los personajes, solo detiene su acción y proporciona retroalimentación visual a los jugadores de que el servidor está regenerando sus personajes. Para demostrar, si examinas ServerScriptService ⟩ SetupHumanoid ⟩ setupHumanoidAsync ⟩ onHumanoidDied, el script establece PlayerState en TaggedOut (notificando esencialmente a ReplicatedStorage ⟩ PlayerStateHandler), y añade algunos indicadores visuales. La lógica real de regeneración es un comportamiento integrado de Roblox.
Cuando los jugadores regeneran de nuevo en la ronda, regeneran en la ubicación de generación de su equipo de acuerdo con la propiedad SpawnLocation.TeamColor. Para personalizar el tiempo de regeneración, puedes añadir la siguiente línea en la parte superior de SetupHumanoid. Para aprender más sobre esta técnica, consulta Players.RespawnTime.
local Players = game:GetService("Players")
Players.RespawnTime = 10 -- nueva línea, en segundosConfiguración miscelánea
Como parte de la configuración inicial, el juego de láser tag de muestra también realiza algunos pasos pequeños, pero críticos:
El juego incluye un script vacío llamado StarterPlayer ⟩ StarterCharacterScripts ⟩ Health que desactiva la regeneración de salud predeterminada de Roblox. Para una explicación del comportamiento de esta propiedad, consulta Humanoid.Health.
El juego utiliza una cámara en primera persona estableciendo la propiedad StarterPlayer.CameraMode.LockFirstPerson. Ten en cuenta que si deseas permitir a los usuarios cambiar entre cámaras en primera y tercera persona, debes cambiar la propiedad programáticamente en lugar de simplemente establecerla una vez en Studio, y modificar los controles y la interfaz de usuario para compensar el cambio de perspectiva.
El juego utiliza la tabla de clasificación integrada de Roblox con la unidad de "puntos", que los jugadores ganan cada vez que etiquetan a otro jugador. Puedes ver la configuración en ServerScriptService ⟩ SetupLeaderboard, pero Tablas de clasificación en el juego ofrece una visión completa. Ten en cuenta que onPlayerTagged añade puntos a la tabla de clasificación, que aprenderás en Añadir rondas y Detectar golpes.
Ahora que los jugadores pueden generar, elegir un blaster y apuntarlo desde un punto de vista en primera persona, la siguiente sección te enseña sobre los scripts detrás de la creación de un juego basado en rondas.