Commit 09de87fa by alsunj

add documentation

parent cb7803e0
Showing with 761 additions and 122 deletions
...@@ -3,37 +3,56 @@ using Unity.Collections; ...@@ -3,37 +3,56 @@ using Unity.Collections;
using Unity.Entities; using Unity.Entities;
using Unity.NetCode; using Unity.NetCode;
[UpdateInGroup(typeof(PredictedSimulationSystemGroup), OrderLast = true)] [UpdateInGroup(typeof(PredictedSimulationSystemGroup), OrderLast = true)]
[UpdateAfter(typeof(CalculateFrameDamageSystem))] [UpdateAfter(typeof(CalculateFrameDamageSystem))]
public partial struct ApplyDamageSystem : ISystem public partial struct ApplyDamageSystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when
/// the required components (NetworkTime and GamePlayingTag) are present.
/// </summary>
/// <param name="state">The system state.</param>
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
state.RequireForUpdate<NetworkTime>(); state.RequireForUpdate<NetworkTime>();
state.RequireForUpdate<GamePlayingTag>(); state.RequireForUpdate<GamePlayingTag>();
} }
/// <summary>
/// Called every frame to update the system. Processes damage for entities
/// with a DamageThisTick buffer and applies it to their CurrentHitPoints.
/// If an entity's hit points drop to zero or below, it is marked for destruction.
/// </summary>
/// <param name="state">The system state.</param>
[BurstCompile] [BurstCompile]
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
// Get the current server tick from the NetworkTime singleton.
var currentTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick; var currentTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
// Create a temporary EntityCommandBuffer to queue entity modifications.
var ecb = new EntityCommandBuffer(Allocator.Temp); var ecb = new EntityCommandBuffer(Allocator.Temp);
// Query entities with CurrentHitPoints, DamageThisTick buffer, and Simulate tag.
foreach (var (currentHitPoints, damageThisTickBuffer, entity) in SystemAPI foreach (var (currentHitPoints, damageThisTickBuffer, entity) in SystemAPI
.Query<RefRW<CurrentHitPoints>, DynamicBuffer<DamageThisTick>>().WithAll<Simulate>() .Query<RefRW<CurrentHitPoints>, DynamicBuffer<DamageThisTick>>().WithAll<Simulate>()
.WithEntityAccess()) .WithEntityAccess())
{ {
// Skip if no damage data exists for the current tick.
if (!damageThisTickBuffer.GetDataAtTick(currentTick, out var damageThisTick)) continue; if (!damageThisTickBuffer.GetDataAtTick(currentTick, out var damageThisTick)) continue;
if (damageThisTick.Tick != currentTick) continue; if (damageThisTick.Tick != currentTick) continue;
// Apply the damage to the entity's current hit points.
currentHitPoints.ValueRW.Value -= damageThisTick.Value; currentHitPoints.ValueRW.Value -= damageThisTick.Value;
// If hit points drop to zero or below, mark the entity for destruction.
if (currentHitPoints.ValueRO.Value <= 0) if (currentHitPoints.ValueRO.Value <= 0)
{ {
ecb.AddComponent<DestroyEntityTag>(entity); ecb.AddComponent<DestroyEntityTag>(entity);
} }
} }
// Apply all queued entity modifications.
ecb.Playback(state.EntityManager); ecb.Playback(state.EntityManager);
} }
} }
\ No newline at end of file
...@@ -2,9 +2,18 @@ using Unity.Burst; ...@@ -2,9 +2,18 @@ using Unity.Burst;
using Unity.Entities; using Unity.Entities;
using Unity.NetCode; using Unity.NetCode;
/// <summary>
/// System responsible for calculating the total damage for the current frame.
/// This system runs in the PredictedSimulationSystemGroup and executes last in the group.
/// </summary>
[UpdateInGroup(typeof(PredictedSimulationSystemGroup), OrderLast = true)] [UpdateInGroup(typeof(PredictedSimulationSystemGroup), OrderLast = true)]
public partial struct CalculateFrameDamageSystem : ISystem public partial struct CalculateFrameDamageSystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when
/// the required components (NetworkTime, GamePlayingTag, and DamageBufferElement) are present.
/// </summary>
/// <param name="state">The system state.</param>
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
state.RequireForUpdate<NetworkTime>(); state.RequireForUpdate<NetworkTime>();
...@@ -12,32 +21,44 @@ public partial struct CalculateFrameDamageSystem : ISystem ...@@ -12,32 +21,44 @@ public partial struct CalculateFrameDamageSystem : ISystem
state.RequireForUpdate<DamageBufferElement>(); state.RequireForUpdate<DamageBufferElement>();
} }
/// <summary>
/// Called every frame to update the system. Aggregates damage from the DamageBufferElement
/// and stores the total damage for the current tick in the DamageThisTick buffer.
/// Clears the damage buffer after processing.
/// </summary>
/// <param name="state">The system state.</param>
[BurstCompile] [BurstCompile]
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
// Retrieve the current server tick from the NetworkTime singleton.
var currentTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick; var currentTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
// Iterate over entities with DamageBufferElement and DamageThisTick buffers, and the Simulate tag.
foreach (var (damageBuffer, damageThisTickBuffer) in SystemAPI foreach (var (damageBuffer, damageThisTickBuffer) in SystemAPI
.Query<DynamicBuffer<DamageBufferElement>, DynamicBuffer<DamageThisTick>>() .Query<DynamicBuffer<DamageBufferElement>, DynamicBuffer<DamageThisTick>>()
.WithAll<Simulate>()) .WithAll<Simulate>())
{ {
// If the damage buffer is empty, add a zero-damage entry for the current tick.
if (damageBuffer.IsEmpty) if (damageBuffer.IsEmpty)
{ {
damageThisTickBuffer.AddCommandData(new DamageThisTick { Tick = currentTick, Value = 0 }); damageThisTickBuffer.AddCommandData(new DamageThisTick { Tick = currentTick, Value = 0 });
} }
else else
{ {
// Calculate the total damage for the current tick.
var totalDamage = 0; var totalDamage = 0;
if (damageThisTickBuffer.GetDataAtTick(currentTick, out var damageThisTick)) if (damageThisTickBuffer.GetDataAtTick(currentTick, out var damageThisTick))
{ {
totalDamage = damageThisTick.Value; totalDamage = damageThisTick.Value;
} }
// Add up all damage values from the damage buffer.
foreach (var damage in damageBuffer) foreach (var damage in damageBuffer)
{ {
totalDamage += damage.Value; totalDamage += damage.Value;
} }
// Store the total damage in the DamageThisTick buffer and clear the damage buffer.
damageThisTickBuffer.AddCommandData(new DamageThisTick { Tick = currentTick, Value = totalDamage }); damageThisTickBuffer.AddCommandData(new DamageThisTick { Tick = currentTick, Value = totalDamage });
damageBuffer.Clear(); damageBuffer.Clear();
} }
......
...@@ -4,10 +4,20 @@ using Unity.Entities; ...@@ -4,10 +4,20 @@ using Unity.Entities;
using Unity.Physics; using Unity.Physics;
using Unity.Physics.Systems; using Unity.Physics.Systems;
/// <summary>
/// System responsible for handling damage when trigger events occur between entities, applies the damage on DamageBufferElement buffer.
/// This system runs within the PhysicsSystemGroup and executes after the PhysicsSimulationGroup.
/// </summary>
[UpdateInGroup(typeof(PhysicsSystemGroup))] [UpdateInGroup(typeof(PhysicsSystemGroup))]
[UpdateAfter(typeof(PhysicsSimulationGroup))] [UpdateAfter(typeof(PhysicsSimulationGroup))]
public partial struct DamageOnTriggerSystem : ISystem public partial struct DamageOnTriggerSystem : ISystem
{ {
/// <summary>
/// Initializes the system by specifying the required components for it to update.
/// Ensures that the system only runs when the SimulationSingleton, EndSimulationEntityCommandBufferSystem.Singleton,
/// and GamePlayingTag components are present.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
state.RequireForUpdate<SimulationSingleton>(); state.RequireForUpdate<SimulationSingleton>();
...@@ -15,11 +25,19 @@ public partial struct DamageOnTriggerSystem : ISystem ...@@ -15,11 +25,19 @@ public partial struct DamageOnTriggerSystem : ISystem
state.RequireForUpdate<GamePlayingTag>(); state.RequireForUpdate<GamePlayingTag>();
} }
/// <summary>
/// Executes the system logic every frame. Schedules the DamageOnTriggerJob to process trigger events
/// and apply damage to entities based on their interactions.
/// </summary>
/// <param name="state">The current state of the system.</param>
[BurstCompile] [BurstCompile]
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
// Retrieve the singleton for the EndSimulationEntityCommandBufferSystem.
var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>(); var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();
var damageOnCollisionJob = new DamageOnTriggerJob
// Create and configure the DamageOnTriggerJob.
var damageOnTriggerJob = new DamageOnTriggerJob
{ {
DamageOnTriggerLookup = SystemAPI.GetComponentLookup<DamageOnTrigger>(true), DamageOnTriggerLookup = SystemAPI.GetComponentLookup<DamageOnTrigger>(true),
AlreadyDamagedLookup = SystemAPI.GetBufferLookup<AlreadyDamagedEntity>(true), AlreadyDamagedLookup = SystemAPI.GetBufferLookup<AlreadyDamagedEntity>(true),
...@@ -27,21 +45,42 @@ public partial struct DamageOnTriggerSystem : ISystem ...@@ -27,21 +45,42 @@ public partial struct DamageOnTriggerSystem : ISystem
ECB = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged) ECB = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged)
}; };
// Retrieve the SimulationSingleton and schedule the job.
var simulationSingleton = SystemAPI.GetSingleton<SimulationSingleton>(); var simulationSingleton = SystemAPI.GetSingleton<SimulationSingleton>();
state.Dependency = damageOnCollisionJob.Schedule(simulationSingleton, state.Dependency); state.Dependency = damageOnTriggerJob.Schedule(simulationSingleton, state.Dependency);
} }
} }
/// <summary>
//team lookup is no more necessary as the enemy only collides with player now. /// Job that processes trigger events and applies damage to entities based on their interactions.
/// </summary>
public struct DamageOnTriggerJob : ITriggerEventsJob public struct DamageOnTriggerJob : ITriggerEventsJob
{ {
/// <summary>
/// Read-only lookup for DamageOnTrigger components to determine damage-dealing entities.
/// </summary>
[ReadOnly] public ComponentLookup<DamageOnTrigger> DamageOnTriggerLookup; [ReadOnly] public ComponentLookup<DamageOnTrigger> DamageOnTriggerLookup;
/// <summary>
/// Lookup for AlreadyDamagedEntity buffers to track entities that have already been damaged.
/// </summary>
public BufferLookup<AlreadyDamagedEntity> AlreadyDamagedLookup; public BufferLookup<AlreadyDamagedEntity> AlreadyDamagedLookup;
/// <summary>
/// Lookup for DamageBufferElement buffers to store damage values for entities.
/// </summary>
public BufferLookup<DamageBufferElement> DamageBufferLookup; public BufferLookup<DamageBufferElement> DamageBufferLookup;
/// <summary>
/// EntityCommandBuffer used to queue entity modifications during the job execution.
/// </summary>
public EntityCommandBuffer ECB; public EntityCommandBuffer ECB;
/// <summary>
/// Executes the job for each trigger event. Determines the damage-dealing and damage-receiving entities,
/// applies damage, and marks entities for destruction if necessary.
/// </summary>
/// <param name="triggerEvent">The trigger event containing the interacting entities.</param>
public void Execute(TriggerEvent triggerEvent) public void Execute(TriggerEvent triggerEvent)
{ {
Entity entityA = triggerEvent.EntityA; Entity entityA = triggerEvent.EntityA;
...@@ -49,6 +88,7 @@ public struct DamageOnTriggerJob : ITriggerEventsJob ...@@ -49,6 +88,7 @@ public struct DamageOnTriggerJob : ITriggerEventsJob
Entity damageDealingEntity = Entity.Null; Entity damageDealingEntity = Entity.Null;
Entity damageReceivingEntity = Entity.Null; Entity damageReceivingEntity = Entity.Null;
// Determine which entity is dealing damage and which is receiving damage.
if (DamageBufferLookup.HasBuffer(entityA) && if (DamageBufferLookup.HasBuffer(entityA) &&
DamageOnTriggerLookup.HasComponent(entityB)) DamageOnTriggerLookup.HasComponent(entityB))
{ {
...@@ -66,6 +106,7 @@ public struct DamageOnTriggerJob : ITriggerEventsJob ...@@ -66,6 +106,7 @@ public struct DamageOnTriggerJob : ITriggerEventsJob
return; return;
} }
// Check if the damage-dealing entity has already damaged the receiving entity.
if (AlreadyDamagedLookup.HasBuffer(damageDealingEntity)) if (AlreadyDamagedLookup.HasBuffer(damageDealingEntity))
{ {
var alreadyDamagedBuffer = AlreadyDamagedLookup[damageDealingEntity]; var alreadyDamagedBuffer = AlreadyDamagedLookup[damageDealingEntity];
...@@ -76,9 +117,11 @@ public struct DamageOnTriggerJob : ITriggerEventsJob ...@@ -76,9 +117,11 @@ public struct DamageOnTriggerJob : ITriggerEventsJob
} }
else else
{ {
// Add a buffer to track already damaged entities if it doesn't exist.
ECB.AddBuffer<AlreadyDamagedEntity>(damageDealingEntity); ECB.AddBuffer<AlreadyDamagedEntity>(damageDealingEntity);
} }
// Apply damage and mark the damage-dealing entity for destruction if applicable.
if (DamageOnTriggerLookup.TryGetComponent(damageDealingEntity, out var damageOnTrigger)) if (DamageOnTriggerLookup.TryGetComponent(damageDealingEntity, out var damageOnTrigger))
{ {
ECB.AddComponent<DestroyEntityTag>(damageDealingEntity); ECB.AddComponent<DestroyEntityTag>(damageDealingEntity);
......
...@@ -3,54 +3,87 @@ using Unity.Transforms; ...@@ -3,54 +3,87 @@ using Unity.Transforms;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
/// <summary>
/// System responsible for managing health bars in the game. This includes spawning health bars for entities,
/// updating their positions and values, and cleaning up health bars when their associated entities are destroyed.
/// Does not have a requirement for GamePlayingTag, so player health can be seen when the players are spawned and the game has not started yet
/// </summary>
[UpdateAfter(typeof(TransformSystemGroup))] [UpdateAfter(typeof(TransformSystemGroup))]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public partial struct HealthBarSystem : ISystem public partial struct HealthBarSystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when the required components
/// (EndSimulationEntityCommandBufferSystem.Singleton and UIPrefabs) are present.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
state.RequireForUpdate<EndSimulationEntityCommandBufferSystem.Singleton>(); state.RequireForUpdate<EndSimulationEntityCommandBufferSystem.Singleton>();
state.RequireForUpdate<UIPrefabs>(); state.RequireForUpdate<UIPrefabs>();
state.RequireForUpdate<GamePlayingTag>();
} }
/// <summary>
/// Called every frame to update the system. Handles the creation, updating, and cleanup of health bars.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
// Retrieve the EntityCommandBuffer for queuing entity modifications.
var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>(); var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged); var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
// Spawn health bars for entities that do not already have one.
foreach (var (transform, healthBarOffset, maxHitPoints, entity) in SystemAPI foreach (var (transform, healthBarOffset, maxHitPoints, entity) in SystemAPI
.Query<LocalTransform, HealthBarOffset, MaxHitPoints>().WithNone<HealthBarUIReference>() .Query<LocalTransform, HealthBarOffset, MaxHitPoints>().WithNone<HealthBarUIReference>()
.WithEntityAccess()) .WithEntityAccess())
{ {
// Instantiate a new health bar at the entity's position plus the offset.
var healthBarPrefab = SystemAPI.ManagedAPI.GetSingleton<UIPrefabs>().PlayerHealthUIEntity; var healthBarPrefab = SystemAPI.ManagedAPI.GetSingleton<UIPrefabs>().PlayerHealthUIEntity;
var spawnPosition = transform.Position + healthBarOffset.Value; var spawnPosition = transform.Position + healthBarOffset.Value;
var newHealthBar = Object.Instantiate(healthBarPrefab, spawnPosition, Quaternion.identity); var newHealthBar = Object.Instantiate(healthBarPrefab, spawnPosition, Quaternion.identity);
// Initialize the health bar with the entity's maximum hit points.
SetHealthBar(newHealthBar, maxHitPoints.Value, maxHitPoints.Value); SetHealthBar(newHealthBar, maxHitPoints.Value, maxHitPoints.Value);
// Add a reference to the health bar in the entity's components.
ecb.AddComponent(entity, new HealthBarUIReference { Value = newHealthBar }); ecb.AddComponent(entity, new HealthBarUIReference { Value = newHealthBar });
} }
// Update position and values of health bar // Update the position and values of existing health bars.
foreach (var (transform, healthBarOffset, currentHitPoints, maxHitPoints, healthBarUI) in SystemAPI foreach (var (transform, healthBarOffset, currentHitPoints, maxHitPoints, healthBarUI) in SystemAPI
.Query<LocalTransform, HealthBarOffset, CurrentHitPoints, MaxHitPoints, HealthBarUIReference>()) .Query<LocalTransform, HealthBarOffset, CurrentHitPoints, MaxHitPoints, HealthBarUIReference>())
{ {
// Update the health bar's position to match the entity's position plus the offset.
var healthBarPosition = transform.Position + healthBarOffset.Value; var healthBarPosition = transform.Position + healthBarOffset.Value;
healthBarUI.Value.transform.position = healthBarPosition; healthBarUI.Value.transform.position = healthBarPosition;
// Update the health bar's slider values to reflect the entity's current and maximum hit points.
SetHealthBar(healthBarUI.Value, currentHitPoints.Value, maxHitPoints.Value); SetHealthBar(healthBarUI.Value, currentHitPoints.Value, maxHitPoints.Value);
} }
// Cleanup health bar once associated entity is destroyed // Cleanup health bars for entities that no longer exist.
foreach (var (healthBarUI, entity) in SystemAPI.Query<HealthBarUIReference>().WithNone<LocalTransform>() foreach (var (healthBarUI, entity) in SystemAPI.Query<HealthBarUIReference>().WithNone<LocalTransform>()
.WithEntityAccess()) .WithEntityAccess())
{ {
// Destroy the health bar GameObject and remove the reference component from the entity.
Object.Destroy(healthBarUI.Value); Object.Destroy(healthBarUI.Value);
ecb.RemoveComponent<HealthBarUIReference>(entity); ecb.RemoveComponent<HealthBarUIReference>(entity);
} }
} }
/// <summary>
/// Configures the health bar's slider to display the current and maximum hit points.
/// </summary>
/// <param name="healthBarCanvasObject">The GameObject representing the health bar.</param>
/// <param name="curHitPoints">The current hit points of the entity.</param>
/// <param name="maxHitPoints">The maximum hit points of the entity.</param>
private void SetHealthBar(GameObject healthBarCanvasObject, int curHitPoints, int maxHitPoints) private void SetHealthBar(GameObject healthBarCanvasObject, int curHitPoints, int maxHitPoints)
{ {
// Retrieve the Slider component from the health bar GameObject.
var healthBarSlider = healthBarCanvasObject.GetComponentInChildren<Slider>(); var healthBarSlider = healthBarCanvasObject.GetComponentInChildren<Slider>();
// Set the slider's minimum, maximum, and current values.
healthBarSlider.minValue = 0; healthBarSlider.minValue = 0;
healthBarSlider.maxValue = maxHitPoints; healthBarSlider.maxValue = maxHitPoints;
healthBarSlider.value = curHitPoints; healthBarSlider.value = curHitPoints;
......
...@@ -4,15 +4,28 @@ using Unity.Physics; ...@@ -4,15 +4,28 @@ using Unity.Physics;
using Unity.Transforms; using Unity.Transforms;
using Unity.NetCode; using Unity.NetCode;
/// <summary>
/// System responsible for handling movement abilities of entities.
/// Updates the velocity of entities based on their movement speed and direction.
/// </summary>
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))] [UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
public partial struct MoveAbilitySystem : ISystem public partial struct MoveAbilitySystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when the required components
/// (GamePlayingTag and AbilityMoveSpeed) are present.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
state.RequireForUpdate<GamePlayingTag>(); state.RequireForUpdate<GamePlayingTag>();
state.RequireForUpdate<AbilityMoveSpeed>(); state.RequireForUpdate<AbilityMoveSpeed>();
} }
/// <summary>
/// Called every frame to update the system. Schedules the MoveAbilityJob to process entity movement in parallel.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
state.Dependency = new MoveAbilityJob() state.Dependency = new MoveAbilityJob()
...@@ -20,10 +33,21 @@ public partial struct MoveAbilitySystem : ISystem ...@@ -20,10 +33,21 @@ public partial struct MoveAbilitySystem : ISystem
} }
} }
/// <summary>
/// Job that processes entity movement by updating their velocity based on their forward direction
/// and movement speed. Excludes entities with the SlimeTag component.
/// </summary>
[BurstCompile] [BurstCompile]
[WithNone(typeof(SlimeTag))] [WithNone(typeof(SlimeTag))]
public partial struct MoveAbilityJob : IJobEntity public partial struct MoveAbilityJob : IJobEntity
{ {
/// <summary>
/// Executes the job for each entity. Calculates the linear velocity of the entity
/// based on its forward direction and movement speed.
/// </summary>
/// <param name="velocity">The physics velocity of the entity.</param>
/// <param name="transform">The local transform of the entity.</param>
/// <param name="moveSpeed">The movement speed of the entity.</param>
[BurstCompile] [BurstCompile]
private void Execute( private void Execute(
ref PhysicsVelocity velocity, ref PhysicsVelocity velocity,
......
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Physics;
using Unity.Transforms;
/// <summary>
/// System responsible for moving slime entities towards their target positions.
/// This system runs in the PredictedSimulationSystemGroup and executes last in the group.
/// </summary>
[UpdateInGroup(typeof(PredictedSimulationSystemGroup), OrderLast = true)]
public partial struct MoveSlimeSystem : ISystem
{
/// <summary>
/// Called when the system is created. Ensures the system only updates when
/// entities with the required components (SlimeTag and GamePlayingTag) exist.
/// </summary>
/// <param name="state">The system state.</param>
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<SlimeTag>();
state.RequireForUpdate<GamePlayingTag>();
}
/// <summary>
/// Called every frame to update the system. Schedules a job to move slime entities
/// towards their target positions based on their move speed and target entity.
/// </summary>
/// <param name="state">The system state.</param>
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Lookup for LocalTransform components, used to get the position of target entities.
var transformLookup = SystemAPI.GetComponentLookup<LocalTransform>(true);
// Schedule the SlimeMoveDirectJob to run in parallel.
state.Dependency = new SlimeMoveDirectJob
{
TransformLookup = transformLookup
}.ScheduleParallel(state.Dependency);
}
/// <summary>
/// Job that handles the movement of slime entities towards their target positions.
/// </summary>
[BurstCompile]
public partial struct SlimeMoveDirectJob : IJobEntity
{
/// <summary>
/// Read-only lookup for LocalTransform components to access target entity positions.
/// </summary>
[ReadOnly] public ComponentLookup<LocalTransform> TransformLookup;
/// <summary>
/// Executes the job for each entity. Updates the entity's velocity to move it
/// towards its target position, or stops it if no valid target exists.
/// </summary>
/// <param name="velocity">The entity's current velocity.</param>
/// <param name="transform">The entity's current transform.</param>
/// <param name="moveSpeed">The entity's movement speed.</param>
/// <param name="targetEntity">The target entity the slime is moving towards.</param>
[BurstCompile]
private void Execute(
ref PhysicsVelocity velocity,
in LocalTransform transform,
in AbilityMoveSpeed moveSpeed,
in NpcTargetEntity targetEntity)
{
// If the target entity is null or does not have a LocalTransform, stop the entity.
if (targetEntity.Value == Entity.Null || !TransformLookup.HasComponent(targetEntity.Value))
{
velocity.Linear = float3.zero;
return;
}
// Calculate the direction towards the target and update the velocity.
var targetPosition = TransformLookup[targetEntity.Value].Position;
var direction = math.normalize(targetPosition - transform.Position);
velocity.Linear = direction * moveSpeed.Value;
}
}
}
\ No newline at end of file
fileFormatVersion: 2 fileFormatVersion: 2
guid: 3783f791f48f7b047a3354bfe6fc88c9 guid: 86f8c4a7fca9c2244981a82be26b0c7d
\ No newline at end of file \ No newline at end of file
...@@ -5,9 +5,18 @@ using Unity.Mathematics; ...@@ -5,9 +5,18 @@ using Unity.Mathematics;
using Unity.NetCode; using Unity.NetCode;
using Unity.Transforms; using Unity.Transforms;
/// <summary>
/// System responsible for handling rogue NPC attacks. This system ensures that NPCs can attack their targets
/// based on cooldowns and attack properties, and updates their transformations accordingly.
/// </summary>
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))] [UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
public partial struct RogueAttackSystem : ISystem public partial struct RogueAttackSystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when the required components
/// (NetworkTime, BeginSimulationEntityCommandBufferSystem.Singleton, and GamePlayingTag) are present.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
state.RequireForUpdate<NetworkTime>(); state.RequireForUpdate<NetworkTime>();
...@@ -15,11 +24,17 @@ public partial struct RogueAttackSystem : ISystem ...@@ -15,11 +24,17 @@ public partial struct RogueAttackSystem : ISystem
state.RequireForUpdate<GamePlayingTag>(); state.RequireForUpdate<GamePlayingTag>();
} }
/// <summary>
/// Called every frame to update the system. Schedules the NpcAttackJob to process NPC attacks in parallel.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
// Retrieve the EntityCommandBuffer singleton and the current network time.
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>(); var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var networkTime = SystemAPI.GetSingleton<NetworkTime>(); var networkTime = SystemAPI.GetSingleton<NetworkTime>();
// Schedule the NpcAttackJob to handle NPC attack logic.
state.Dependency = new NpcAttackJob state.Dependency = new NpcAttackJob
{ {
CurrentTick = networkTime.ServerTick, CurrentTick = networkTime.ServerTick,
...@@ -29,51 +44,85 @@ public partial struct RogueAttackSystem : ISystem ...@@ -29,51 +44,85 @@ public partial struct RogueAttackSystem : ISystem
} }
} }
/// <summary>
/// Job that processes NPC attack logic. Handles attack cooldowns, target rotation, and attack instantiation.
/// Excludes entities with the SlimeTag component.
/// </summary>
[BurstCompile] [BurstCompile]
[WithAll(typeof(Simulate))] [WithAll(typeof(Simulate))]
[WithNone(typeof(SlimeTag))] [WithNone(typeof(SlimeTag))]
public partial struct NpcAttackJob : IJobEntity public partial struct NpcAttackJob : IJobEntity
{ {
/// <summary>
/// The current server tick used to determine attack cooldowns.
/// </summary>
[ReadOnly] public NetworkTick CurrentTick; [ReadOnly] public NetworkTick CurrentTick;
/// <summary>
/// Read-only lookup for LocalTransform components to access entity positions and rotations.
/// </summary>
[ReadOnly] public ComponentLookup<LocalTransform> TransformLookup; [ReadOnly] public ComponentLookup<LocalTransform> TransformLookup;
/// <summary>
/// Parallel writer for queuing entity modifications during the job execution.
/// </summary>
public EntityCommandBuffer.ParallelWriter ECB; public EntityCommandBuffer.ParallelWriter ECB;
/// <summary>
/// Executes the job for each entity. Handles attack cooldowns, rotates the NPC towards its target,
/// and spawns attack entities with the appropriate transformations.
/// </summary>
/// <param name="attackCooldown">The buffer storing attack cooldown data for the NPC.</param>
/// <param name="attackProperties">The properties defining the NPC's attack behavior.</param>
/// <param name="targetEntity">The target entity the NPC is attacking.</param>
/// <param name="enemyEntity">The NPC entity performing the attack.</param>
/// <param name="team">The team type of the NPC.</param>
/// <param name="sortKey">The chunk index used for parallel execution.</param>
[BurstCompile] [BurstCompile]
private void Execute(ref DynamicBuffer<NpcAttackCooldown> attackCooldown, in NpcAttackProperties attackProperties, private void Execute(
in NpcTargetEntity targetEntity, Entity enemyEntity, TeamTypes team, [ChunkIndexInQuery] int sortKey) ref DynamicBuffer<NpcAttackCooldown> attackCooldown,
in NpcAttackProperties attackProperties,
in NpcTargetEntity targetEntity,
Entity enemyEntity,
TeamTypes team,
[ChunkIndexInQuery] int sortKey)
{ {
// Ensure the target entity has a LocalTransform component.
if (!TransformLookup.HasComponent(targetEntity.Value)) return; if (!TransformLookup.HasComponent(targetEntity.Value)) return;
// Retrieve or initialize the cooldown expiration tick.
if (!attackCooldown.GetDataAtTick(CurrentTick, out var cooldownExpirationTick)) if (!attackCooldown.GetDataAtTick(CurrentTick, out var cooldownExpirationTick))
{ {
cooldownExpirationTick.Value = NetworkTick.Invalid; cooldownExpirationTick.Value = NetworkTick.Invalid;
} }
// Check if the NPC can attack based on the cooldown.
var canAttack = !cooldownExpirationTick.Value.IsValid || var canAttack = !cooldownExpirationTick.Value.IsValid ||
CurrentTick.IsNewerThan(cooldownExpirationTick.Value); CurrentTick.IsNewerThan(cooldownExpirationTick.Value);
if (!canAttack) return; if (!canAttack) return;
// Retrieve the enemy's and target's transformations.
var enemyTransform = TransformLookup[enemyEntity]; var enemyTransform = TransformLookup[enemyEntity];
var spawnPosition = enemyTransform.Position;
var targetPosition = TransformLookup[targetEntity.Value].Position; var targetPosition = TransformLookup[targetEntity.Value].Position;
// Get the vector from the enemy to the player // Calculate the direction and rotation towards the target.
var direction = math.normalize(targetPosition - spawnPosition); var direction = math.normalize(targetPosition - enemyTransform.Position);
var targetRotation = quaternion.LookRotationSafe(direction, math.up()); var targetRotation = quaternion.LookRotationSafe(direction, math.up());
// Rotate the enemy towards player // Rotate the NPC towards the target.
enemyTransform.Rotation = targetRotation; enemyTransform.Rotation = targetRotation;
ECB.SetComponent(sortKey, enemyEntity, enemyTransform); ECB.SetComponent(sortKey, enemyEntity, enemyTransform);
// Spawn the attack entity at the appropriate position and rotation.
var newAttack = ECB.Instantiate(sortKey, attackProperties.AttackPrefab); var newAttack = ECB.Instantiate(sortKey, attackProperties.AttackPrefab);
var newAttackTransform = LocalTransform.FromPositionRotation(spawnPosition + attackProperties.FirePointOffset, var newAttackTransform = LocalTransform.FromPositionRotation(
quaternion.LookRotationSafe(targetPosition - spawnPosition, math.up())); enemyTransform.Position + attackProperties.FirePointOffset,
quaternion.LookRotationSafe(targetPosition - enemyTransform.Position, math.up()));
ECB.SetComponent(sortKey, newAttack, newAttackTransform); ECB.SetComponent(sortKey, newAttack, newAttackTransform);
ECB.SetComponent(sortKey, newAttack, team); ECB.SetComponent(sortKey, newAttack, team);
// Update the attack cooldown.
var newCooldownTick = CurrentTick; var newCooldownTick = CurrentTick;
newCooldownTick.Add(attackProperties.CooldownTickCount); newCooldownTick.Add(attackProperties.CooldownTickCount);
attackCooldown.AddCommandData(new NpcAttackCooldown { Tick = CurrentTick, Value = newCooldownTick }); attackCooldown.AddCommandData(new NpcAttackCooldown { Tick = CurrentTick, Value = newCooldownTick });
......
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Physics;
using Unity.Transforms;
[UpdateInGroup(typeof(PredictedSimulationSystemGroup), OrderLast = true)]
public partial struct MoveSlimeSystem : ISystem
{
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<BeginSimulationEntityCommandBufferSystem.Singleton>();
state.RequireForUpdate<SlimeTag>();
state.RequireForUpdate<GamePlayingTag>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var transformLookup = SystemAPI.GetComponentLookup<LocalTransform>(true);
state.Dependency = new SlimeMoveDirectJob
{
TransformLookup = transformLookup
}.ScheduleParallel(state.Dependency);
}
[BurstCompile]
public partial struct SlimeMoveDirectJob : IJobEntity
{
[ReadOnly] public ComponentLookup<LocalTransform> TransformLookup;
[BurstCompile]
private void Execute(
ref PhysicsVelocity velocity,
in LocalTransform transform,
in AbilityMoveSpeed moveSpeed,
in NpcTargetEntity targetEntity)
{
if (targetEntity.Value == Entity.Null || !TransformLookup.HasComponent(targetEntity.Value))
{
velocity.Linear = float3.zero;
return;
}
var targetPosition = TransformLookup[targetEntity.Value].Position;
var direction = math.normalize(targetPosition - transform.Position);
velocity.Linear = direction * moveSpeed.Value;
}
}
}
\ No newline at end of file
fileFormatVersion: 2
guid: 86f8c4a7fca9c2244981a82be26b0c7d
\ No newline at end of file
...@@ -5,13 +5,25 @@ using Unity.Physics; ...@@ -5,13 +5,25 @@ using Unity.Physics;
using Unity.Physics.Systems; using Unity.Physics.Systems;
using Unity.Transforms; using Unity.Transforms;
/// <summary>
/// System responsible for targeting logic for NPCs. This system determines the closest valid target
/// within a specified radius for each NPC and updates the target entity accordingly.
/// </summary>
[UpdateInGroup(typeof(PhysicsSystemGroup))] [UpdateInGroup(typeof(PhysicsSystemGroup))]
[UpdateAfter(typeof(PhysicsSimulationGroup))] [UpdateAfter(typeof(PhysicsSimulationGroup))]
[UpdateBefore(typeof(ExportPhysicsWorld))] [UpdateBefore(typeof(ExportPhysicsWorld))]
public partial struct TargetingSystem : ISystem public partial struct TargetingSystem : ISystem
{ {
/// <summary>
/// Collision filter used to define which layers the NPCs can target.
/// </summary>
private CollisionFilter _npcAttackFilter; private CollisionFilter _npcAttackFilter;
/// <summary>
/// Called when the system is created. Ensures the system only updates when the required components
/// (PhysicsWorldSingleton and GamePlayingTag) are present. Initializes the collision filter for NPC targeting.
/// </summary>
/// <param name="state">The current state of the system.</param>
[BurstCompile] [BurstCompile]
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
...@@ -19,12 +31,15 @@ public partial struct TargetingSystem : ISystem ...@@ -19,12 +31,15 @@ public partial struct TargetingSystem : ISystem
state.RequireForUpdate<GamePlayingTag>(); state.RequireForUpdate<GamePlayingTag>();
_npcAttackFilter = new CollisionFilter _npcAttackFilter = new CollisionFilter
{ {
BelongsTo = 1 << 6, //Target Cast BelongsTo = 1 << 6, // Target Cast
CollidesWith = 1 << 1 //Player CollidesWith = 1 << 1 // Player
}; };
} }
/// <summary>
/// Called every frame to update the system. Schedules the NpcTargetingJob to process NPC targeting logic in parallel.
/// </summary>
/// <param name="state">The current state of the system.</param>
[BurstCompile] [BurstCompile]
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
...@@ -36,26 +51,51 @@ public partial struct TargetingSystem : ISystem ...@@ -36,26 +51,51 @@ public partial struct TargetingSystem : ISystem
}.ScheduleParallel(state.Dependency); }.ScheduleParallel(state.Dependency);
} }
/// <summary>
/// Job that processes NPC targeting logic. Identifies the closest valid target within the NPC's targeting radius
/// and updates the target entity component.
/// </summary>
[BurstCompile] [BurstCompile]
[WithAll(typeof(Simulate))] [WithAll(typeof(Simulate))]
public partial struct NpcTargetingJob : IJobEntity public partial struct NpcTargetingJob : IJobEntity
{ {
/// <summary>
/// The collision world used to perform overlap sphere queries for detecting potential targets.
/// </summary>
[ReadOnly] public CollisionWorld CollisionWorld; [ReadOnly] public CollisionWorld CollisionWorld;
/// <summary>
/// The collision filter defining the layers that NPCs can target.
/// </summary>
[ReadOnly] public CollisionFilter CollisionFilter; [ReadOnly] public CollisionFilter CollisionFilter;
/// <summary>
/// Read-only lookup for TeamTypes components to ensure NPCs do not target entities on the same team.
/// </summary>
[ReadOnly] public ComponentLookup<TeamTypes> TeamTypeLookup; [ReadOnly] public ComponentLookup<TeamTypes> TeamTypeLookup;
/// <summary>
/// Executes the job for each NPC entity. Finds the closest valid target within the targeting radius
/// and updates the NpcTargetEntity component with the target entity.
/// </summary>
/// <param name="enemyEntity">The NPC entity performing the targeting.</param>
/// <param name="targetEntity">The component storing the current target entity for the NPC.</param>
/// <param name="transform">The local transform of the NPC entity.</param>
/// <param name="targetRadius">The targeting radius of the NPC.</param>
[BurstCompile] [BurstCompile]
private void Execute(Entity enemyEntity, ref NpcTargetEntity targetEntity, in LocalTransform transform, private void Execute(Entity enemyEntity, ref NpcTargetEntity targetEntity, in LocalTransform transform,
in NpcTargetRadius targetRadius) in NpcTargetRadius targetRadius)
{ {
// Temporary list to store potential target hits.
var hits = new NativeList<DistanceHit>(Allocator.TempJob); var hits = new NativeList<DistanceHit>(Allocator.TempJob);
// Perform an overlap sphere query to find potential targets within the targeting radius.
if (CollisionWorld.OverlapSphere(transform.Position, targetRadius.Value, ref hits, CollisionFilter)) if (CollisionWorld.OverlapSphere(transform.Position, targetRadius.Value, ref hits, CollisionFilter))
{ {
var closestDistance = float.MaxValue; var closestDistance = float.MaxValue;
var closestEntity = Entity.Null; var closestEntity = Entity.Null;
// Iterate through the hits to find the closest valid target.
foreach (var hit in hits) foreach (var hit in hits)
{ {
if (!TeamTypeLookup.TryGetComponent(hit.Entity, out var teamTypes)) continue; if (!TeamTypeLookup.TryGetComponent(hit.Entity, out var teamTypes)) continue;
...@@ -67,13 +107,16 @@ public partial struct TargetingSystem : ISystem ...@@ -67,13 +107,16 @@ public partial struct TargetingSystem : ISystem
} }
} }
// Update the target entity with the closest valid target.
targetEntity.Value = closestEntity; targetEntity.Value = closestEntity;
} }
else else
{ {
// If no valid targets are found, set the target entity to null.
targetEntity.Value = Entity.Null; targetEntity.Value = Entity.Null;
} }
// Dispose of the temporary hits list.
hits.Dispose(); hits.Dispose();
} }
} }
......
...@@ -4,9 +4,19 @@ using Unity.NetCode; ...@@ -4,9 +4,19 @@ using Unity.NetCode;
using UnityEngine; using UnityEngine;
/// <summary>
/// System responsible for handling game entry requests from client players.
/// This system ensures that clients are marked as "in-game" and initializes their game-related components,
/// such as creating a camera and associating it with the player.
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public partial struct ClientRequestGameEntrySystem : ISystem public partial struct ClientRequestGameEntrySystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when there are entities
/// with a `NetworkId` component but without a `NetworkStreamInGame` component.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
EntityQueryBuilder entityQueryBuilder = new EntityQueryBuilder(Allocator.Temp) EntityQueryBuilder entityQueryBuilder = new EntityQueryBuilder(Allocator.Temp)
...@@ -16,35 +26,56 @@ public partial struct ClientRequestGameEntrySystem : ISystem ...@@ -16,35 +26,56 @@ public partial struct ClientRequestGameEntrySystem : ISystem
entityQueryBuilder.Dispose(); entityQueryBuilder.Dispose();
} }
/// <summary>
/// Called every frame to process game entry requests. Marks clients as "in-game",
/// creates a camera for each client, and sends an RPC request to notify the server.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
EntityCommandBuffer entityCommandBuffer = new EntityCommandBuffer(Unity.Collections.Allocator.Temp); EntityCommandBuffer entityCommandBuffer = new EntityCommandBuffer(Unity.Collections.Allocator.Temp);
// Iterate through all entities with a `NetworkId` component but without a `NetworkStreamInGame` component.
foreach (( foreach ((
RefRO<NetworkId> networkId, RefRO<NetworkId> networkId,
Entity entity) Entity entity)
in SystemAPI.Query in SystemAPI.Query
<RefRO<NetworkId>>().WithNone<NetworkStreamInGame>().WithEntityAccess()) <RefRO<NetworkId>>().WithNone<NetworkStreamInGame>().WithEntityAccess())
{ {
// Add the `NetworkStreamInGame` component to mark the client as "in-game".
entityCommandBuffer.AddComponent<NetworkStreamInGame>(entity); entityCommandBuffer.AddComponent<NetworkStreamInGame>(entity);
// Create a new entity for the game entry request.
var requestGameConnection = entityCommandBuffer.CreateEntity(); var requestGameConnection = entityCommandBuffer.CreateEntity();
// Create a new camera GameObject for the client and associate it with the player.
GameObject playerCameraGO = new GameObject($"Camera{networkId.ValueRO.Value}"); GameObject playerCameraGO = new GameObject($"Camera{networkId.ValueRO.Value}");
playerCameraGO.AddComponent<Camera>(); playerCameraGO.AddComponent<Camera>();
FollowPlayer followScript = playerCameraGO.AddComponent<FollowPlayer>(); FollowPlayer followScript = playerCameraGO.AddComponent<FollowPlayer>();
followScript.networkId = networkId.ValueRO.Value; followScript.networkId = networkId.ValueRO.Value;
// Add components to the request entity to send an RPC to the server.
entityCommandBuffer.AddComponent<GoInGameRequestRpc>(requestGameConnection); entityCommandBuffer.AddComponent<GoInGameRequestRpc>(requestGameConnection);
entityCommandBuffer.AddComponent<SendRpcCommandRequest>(requestGameConnection); entityCommandBuffer.AddComponent<SendRpcCommandRequest>(requestGameConnection);
} }
// Apply all queued entity modifications.
entityCommandBuffer.Playback(state.EntityManager); entityCommandBuffer.Playback(state.EntityManager);
} }
} }
/// <summary>
/// System responsible for handling game entry requests from thin clients.
/// This system ensures that thin clients are marked as "in-game" and sends an RPC request to notify the server.
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ThinClientSimulation)] [WorldSystemFilter(WorldSystemFilterFlags.ThinClientSimulation)]
public partial struct ThinClientRequestGameEntrySystem : ISystem public partial struct ThinClientRequestGameEntrySystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when there are entities
/// with a `NetworkId` component but without a `NetworkStreamInGame` component.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
EntityQueryBuilder entityQueryBuilder = new EntityQueryBuilder(Allocator.Temp) EntityQueryBuilder entityQueryBuilder = new EntityQueryBuilder(Allocator.Temp)
...@@ -54,24 +85,34 @@ public partial struct ThinClientRequestGameEntrySystem : ISystem ...@@ -54,24 +85,34 @@ public partial struct ThinClientRequestGameEntrySystem : ISystem
entityQueryBuilder.Dispose(); entityQueryBuilder.Dispose();
} }
/// <summary>
/// Called every frame to process game entry requests. Marks thin clients as "in-game"
/// and sends an RPC request to notify the server.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
EntityCommandBuffer entityCommandBuffer = new EntityCommandBuffer(Unity.Collections.Allocator.Temp); EntityCommandBuffer entityCommandBuffer = new EntityCommandBuffer(Unity.Collections.Allocator.Temp);
// Iterate through all entities with a `NetworkId` component but without a `NetworkStreamInGame` component.
foreach (( foreach ((
RefRO<NetworkId> networkId, RefRO<NetworkId> networkId,
Entity entity) Entity entity)
in SystemAPI.Query in SystemAPI.Query
<RefRO<NetworkId>>().WithNone<NetworkStreamInGame>().WithEntityAccess()) <RefRO<NetworkId>>().WithNone<NetworkStreamInGame>().WithEntityAccess())
{ {
// Add the `NetworkStreamInGame` component to mark the thin client as "in-game".
entityCommandBuffer.AddComponent<NetworkStreamInGame>(entity); entityCommandBuffer.AddComponent<NetworkStreamInGame>(entity);
// Create a new entity for the game entry request.
var requestGameConnection = entityCommandBuffer.CreateEntity(); var requestGameConnection = entityCommandBuffer.CreateEntity();
// Add components to the request entity to send an RPC to the server.
entityCommandBuffer.AddComponent<GoInGameRequestRpc>(requestGameConnection); entityCommandBuffer.AddComponent<GoInGameRequestRpc>(requestGameConnection);
entityCommandBuffer.AddComponent<SendRpcCommandRequest>(requestGameConnection); entityCommandBuffer.AddComponent<SendRpcCommandRequest>(requestGameConnection);
} }
// Apply all queued entity modifications.
entityCommandBuffer.Playback(state.EntityManager); entityCommandBuffer.Playback(state.EntityManager);
} }
} }
\ No newline at end of file
...@@ -4,38 +4,65 @@ using Unity.Collections; ...@@ -4,38 +4,65 @@ using Unity.Collections;
using Unity.Entities; using Unity.Entities;
using Unity.NetCode; using Unity.NetCode;
/// <summary>
/// System responsible for handling client-side game start logic. This system processes RPCs related to the
/// number of players remaining to start the game and the game start tick, triggering appropriate actions
/// and updating the game state on the client.
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public partial class ClientStartGameSystem : SystemBase public partial class ClientStartGameSystem : SystemBase
{ {
/// <summary>
/// Action invoked when the number of players remaining to start the game is updated.
/// </summary>
public Action<int> OnUpdatePlayersRemainingToStart; public Action<int> OnUpdatePlayersRemainingToStart;
/// <summary>
/// Action invoked when the game start countdown begins.
/// </summary>
public Action OnStartGameCountdown; public Action OnStartGameCountdown;
/// <summary>
/// Called every frame to process game start-related RPCs. Handles the destruction of RPC entities,
/// updates the number of players remaining to start, and initializes the game start tick on the client.
/// </summary>
protected override void OnUpdate() protected override void OnUpdate()
{ {
// Create a temporary EntityCommandBuffer to queue entity modifications.
var ecb = new EntityCommandBuffer(Allocator.Temp); var ecb = new EntityCommandBuffer(Allocator.Temp);
// Process RPCs for updating the number of players remaining to start the game.
foreach (var (playersRemainingToStart, entity) in SystemAPI.Query<PlayersRemainingToStart>() foreach (var (playersRemainingToStart, entity) in SystemAPI.Query<PlayersRemainingToStart>()
.WithAll<ReceiveRpcCommandRequest>().WithEntityAccess()) .WithAll<ReceiveRpcCommandRequest>().WithEntityAccess())
{ {
// Destroy the RPC entity after processing.
ecb.DestroyEntity(entity); ecb.DestroyEntity(entity);
// Invoke the action to update the number of players remaining to start.
OnUpdatePlayersRemainingToStart?.Invoke(playersRemainingToStart.Value); OnUpdatePlayersRemainingToStart?.Invoke(playersRemainingToStart.Value);
} }
// Process RPCs for the game start tick.
foreach (var (gameStartTick, entity) in SystemAPI.Query<GameStartTickRpc>() foreach (var (gameStartTick, entity) in SystemAPI.Query<GameStartTickRpc>()
.WithAll<ReceiveRpcCommandRequest>().WithEntityAccess()) .WithAll<ReceiveRpcCommandRequest>().WithEntityAccess())
{ {
// Destroy the RPC entity after processing.
ecb.DestroyEntity(entity); ecb.DestroyEntity(entity);
// Invoke the action to start the game countdown.
OnStartGameCountdown?.Invoke(); OnStartGameCountdown?.Invoke();
// Create a new entity to store the game start tick on the client side.
var gameStartEntity = ecb.CreateEntity(); var gameStartEntity = ecb.CreateEntity();
//creates the entity about when the game has started on client side // Add the GameStartTick component to the new entity with the received tick value.
ecb.AddComponent(gameStartEntity, new GameStartTick ecb.AddComponent(gameStartEntity, new GameStartTick
{ {
Value = gameStartTick.Value Value = gameStartTick.Value
}); });
} }
// Apply all queued entity modifications.
ecb.Playback(EntityManager); ecb.Playback(EntityManager);
} }
} }
\ No newline at end of file
...@@ -5,46 +5,79 @@ using Unity.Entities; ...@@ -5,46 +5,79 @@ using Unity.Entities;
using Unity.Mathematics; using Unity.Mathematics;
using Unity.NetCode; using Unity.NetCode;
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))] [UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
public partial class CountdownToGameStartSystem : SystemBase public partial class CountdownToGameStartSystem : SystemBase
{ {
/// <summary>
/// Action invoked to update the countdown text with the number of seconds remaining until the game starts.
/// </summary>
public Action<int> OnUpdateCountdownText; public Action<int> OnUpdateCountdownText;
/// <summary>
/// Action invoked when the countdown ends and the game starts.
/// </summary>
public Action OnCountdownEnd; public Action OnCountdownEnd;
/// <summary>
/// Called when the system is created. Ensures the system only updates when a `NetworkTime` component is present.
/// </summary>
protected override void OnCreate() protected override void OnCreate()
{ {
RequireForUpdate<NetworkTime>(); RequireForUpdate<NetworkTime>();
} }
/// <summary>
/// Called every frame to process the countdown to the game start. Updates the countdown text or triggers
/// the game start logic when the countdown ends.
/// </summary>
protected override void OnUpdate() protected override void OnUpdate()
{ {
// Retrieve the current network time.
var networkTime = SystemAPI.GetSingleton<NetworkTime>(); var networkTime = SystemAPI.GetSingleton<NetworkTime>();
// Skip processing if this is not the first time fully predicting the current tick.
if (!networkTime.IsFirstTimeFullyPredictingTick) return; if (!networkTime.IsFirstTimeFullyPredictingTick) return;
// Get the current server tick.
var currentTick = networkTime.ServerTick; var currentTick = networkTime.ServerTick;
// Create a temporary EntityCommandBuffer to queue entity modifications.
var ecb = new EntityCommandBuffer(Allocator.Temp); var ecb = new EntityCommandBuffer(Allocator.Temp);
// Iterate through all entities with a `GameStartTick` component and a `Simulate` tag.
foreach (var (gameStartTick, entity) in SystemAPI.Query<GameStartTick>().WithAll<Simulate>().WithEntityAccess()) foreach (var (gameStartTick, entity) in SystemAPI.Query<GameStartTick>().WithAll<Simulate>().WithEntityAccess())
{ {
// Check if the current tick is equal to or newer than the game start tick.
if (currentTick.Equals(gameStartTick.Value) || currentTick.IsNewerThan(gameStartTick.Value)) if (currentTick.Equals(gameStartTick.Value) || currentTick.IsNewerThan(gameStartTick.Value))
{ {
// Create a new entity to represent the game playing state.
var gamePlayingEntity = ecb.CreateEntity(); var gamePlayingEntity = ecb.CreateEntity();
ecb.SetName(gamePlayingEntity, "GamePlayingEntity"); ecb.SetName(gamePlayingEntity, "GamePlayingEntity");
ecb.AddComponent<GamePlayingTag>(gamePlayingEntity); ecb.AddComponent<GamePlayingTag>(gamePlayingEntity);
// Destroy the `GameStartTick` entity as the countdown has ended.
ecb.DestroyEntity(entity); ecb.DestroyEntity(entity);
// Invoke the action to signal the end of the countdown.
OnCountdownEnd?.Invoke(); OnCountdownEnd?.Invoke();
} }
else else
{ {
// Calculate the number of ticks remaining until the game starts.
var ticksToStart = gameStartTick.Value.TickIndexForValidTick - currentTick.TickIndexForValidTick; var ticksToStart = gameStartTick.Value.TickIndexForValidTick - currentTick.TickIndexForValidTick;
// Retrieve the simulation tick rate.
var simulationTickRate = NetCodeConfig.Global.ClientServerTickRate.SimulationTickRate; var simulationTickRate = NetCodeConfig.Global.ClientServerTickRate.SimulationTickRate;
// Calculate the number of seconds remaining until the game starts.
var secondsToStart = (int)math.ceil((float)ticksToStart / simulationTickRate); var secondsToStart = (int)math.ceil((float)ticksToStart / simulationTickRate);
// Invoke the action to update the countdown text.
OnUpdateCountdownText?.Invoke(secondsToStart); OnUpdateCountdownText?.Invoke(secondsToStart);
} }
} }
// Apply all queued entity modifications.
ecb.Playback(EntityManager); ecb.Playback(EntityManager);
} }
} }
\ No newline at end of file
...@@ -8,6 +8,11 @@ using UnityEngine; ...@@ -8,6 +8,11 @@ using UnityEngine;
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
partial struct GoInGameServerSystem : ISystem partial struct GoInGameServerSystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when the required components
/// and entities are present in the world.
/// </summary>
/// <param name="state">The current state of the system.</param>
[BurstCompile] [BurstCompile]
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
...@@ -19,63 +24,81 @@ partial struct GoInGameServerSystem : ISystem ...@@ -19,63 +24,81 @@ partial struct GoInGameServerSystem : ISystem
state.RequireForUpdate<GoInGameRequestRpc>(); state.RequireForUpdate<GoInGameRequestRpc>();
} }
/// <summary>
/// Called every frame to process incoming game entry requests from clients. Handles player instantiation,
/// updates the player counter, and sends RPCs to clients about the game start or remaining players.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
// Create a temporary command buffer to queue entity modifications.
EntityCommandBuffer entityCommandBuffer = new EntityCommandBuffer(Unity.Collections.Allocator.Temp); EntityCommandBuffer entityCommandBuffer = new EntityCommandBuffer(Unity.Collections.Allocator.Temp);
// Retrieve singleton components and entities required for processing.
EntititesReferences entititesReferences = SystemAPI.GetSingleton<EntititesReferences>(); EntititesReferences entititesReferences = SystemAPI.GetSingleton<EntititesReferences>();
Entity gameStartPropertiesEntity = SystemAPI.GetSingletonEntity<GameStartProperties>(); Entity gameStartPropertiesEntity = SystemAPI.GetSingletonEntity<GameStartProperties>();
PlayerCounter playerCounter = SystemAPI.GetComponent<PlayerCounter>(gameStartPropertiesEntity); PlayerCounter playerCounter = SystemAPI.GetComponent<PlayerCounter>(gameStartPropertiesEntity);
GameStartProperties gameStartProperties = GameStartProperties gameStartProperties =
SystemAPI.GetComponent<GameStartProperties>(gameStartPropertiesEntity); SystemAPI.GetComponent<GameStartProperties>(gameStartPropertiesEntity);
// Iterate through all entities with a `ReceiveRpcCommandRequest` and `GoInGameRequestRpc` component.
foreach (( foreach ((
RefRO<ReceiveRpcCommandRequest> receiveRpcCommandRequest, RefRO<ReceiveRpcCommandRequest> receiveRpcCommandRequest,
Entity entity) in Entity entity) in
SystemAPI.Query SystemAPI.Query
<RefRO<ReceiveRpcCommandRequest>>().WithAll<GoInGameRequestRpc>().WithEntityAccess()) <RefRO<ReceiveRpcCommandRequest>>().WithAll<GoInGameRequestRpc>().WithEntityAccess())
{ {
// Get the source connection entity from the RPC request.
Entity sourceConnection = Entity sourceConnection =
receiveRpcCommandRequest.ValueRO.SourceConnection; // Get the source connection entity receiveRpcCommandRequest.ValueRO.SourceConnection;
// Mark the client as "in-game" by adding the `NetworkStreamInGame` component.
entityCommandBuffer.AddComponent<NetworkStreamInGame>(sourceConnection); entityCommandBuffer.AddComponent<NetworkStreamInGame>(sourceConnection);
Debug.Log("Client Connected to Server"); Debug.Log("Client Connected to Server");
entityCommandBuffer.DestroyEntity(entity);
// Destroy the RPC entity after processing.
entityCommandBuffer.DestroyEntity(entity);
// Instantiate player entity and place randomly on the x axis -+10 // Instantiate a player entity and set its position randomly along the x-axis.
Entity playerEntity = entityCommandBuffer.Instantiate(entititesReferences.PlayerPrefabEntity); Entity playerEntity = entityCommandBuffer.Instantiate(entititesReferences.PlayerPrefabEntity);
entityCommandBuffer.SetComponent(playerEntity, LocalTransform.FromPosition(new float3( entityCommandBuffer.SetComponent(playerEntity, LocalTransform.FromPosition(new float3(
UnityEngine.Random.Range(-10, +10), 0, 0))); UnityEngine.Random.Range(-10, +10), 0, 0)));
NetworkId networkId = SystemAPI.GetComponent<NetworkId>(sourceConnection); // use sourceConnection // Retrieve the `NetworkId` component from the source connection and assign it to the player entity.
NetworkId networkId = SystemAPI.GetComponent<NetworkId>(sourceConnection);
entityCommandBuffer.AddComponent(playerEntity, new GhostOwner entityCommandBuffer.AddComponent(playerEntity, new GhostOwner
{ {
NetworkId = networkId.Value NetworkId = networkId.Value
}); });
entityCommandBuffer.AddComponent(playerEntity, new NetworkEntityReference { Value = sourceConnection }); entityCommandBuffer.AddComponent(playerEntity, new NetworkEntityReference { Value = sourceConnection });
entityCommandBuffer.AppendToBuffer(sourceConnection, new LinkedEntityGroup // use sourceConnection // Link the player entity to the source connection using a `LinkedEntityGroup` buffer.
entityCommandBuffer.AppendToBuffer(sourceConnection, new LinkedEntityGroup
{ {
Value = playerEntity Value = playerEntity
}); });
// Update the player counter and calculate the number of players remaining to start the game.
playerCounter.Value++; playerCounter.Value++;
int playersRemainingToStart = gameStartProperties.PlayerAmount - playerCounter.Value; int playersRemainingToStart = gameStartProperties.PlayerAmount - playerCounter.Value;
// Create an RPC entity to notify clients about the game start or remaining players.
var gameStartRpc = entityCommandBuffer.CreateEntity(); var gameStartRpc = entityCommandBuffer.CreateEntity();
if (playersRemainingToStart <= 0 && !SystemAPI.HasSingleton<GamePlayingTag>()) if (playersRemainingToStart <= 0 && !SystemAPI.HasSingleton<GamePlayingTag>())
{ {
// Calculate the tick at which the game should start.
var simulationTickRate = NetCodeConfig.Global.ClientServerTickRate.SimulationTickRate; var simulationTickRate = NetCodeConfig.Global.ClientServerTickRate.SimulationTickRate;
var ticksUntilStart = (uint)(simulationTickRate * gameStartProperties.CountdownTime); var ticksUntilStart = (uint)(simulationTickRate * gameStartProperties.CountdownTime);
var gameStartTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick; var gameStartTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
gameStartTick.Add(ticksUntilStart); gameStartTick.Add(ticksUntilStart);
// sends data to client about on what tick the game should start. // Add a `GameStartTickRpc` component to the RPC entity with the calculated start tick.
entityCommandBuffer.AddComponent(gameStartRpc, new GameStartTickRpc entityCommandBuffer.AddComponent(gameStartRpc, new GameStartTickRpc
{ {
Value = gameStartTick Value = gameStartTick
}); });
//creates the entity about when the game has started on server side // Create a server-side entity to track the game start tick.
var gameStartEntity = entityCommandBuffer.CreateEntity(); var gameStartEntity = entityCommandBuffer.CreateEntity();
entityCommandBuffer.AddComponent(gameStartEntity, new GameStartTick entityCommandBuffer.AddComponent(gameStartEntity, new GameStartTick
{ {
...@@ -84,14 +107,19 @@ partial struct GoInGameServerSystem : ISystem ...@@ -84,14 +107,19 @@ partial struct GoInGameServerSystem : ISystem
} }
else else
{ {
// Add a `PlayersRemainingToStart` component to the RPC entity with the remaining player count.
entityCommandBuffer.AddComponent(gameStartRpc, entityCommandBuffer.AddComponent(gameStartRpc,
new PlayersRemainingToStart { Value = playersRemainingToStart }); new PlayersRemainingToStart { Value = playersRemainingToStart });
} }
// Add a `SendRpcCommandRequest` component to the RPC entity to send it to clients.
entityCommandBuffer.AddComponent<SendRpcCommandRequest>(gameStartRpc); entityCommandBuffer.AddComponent<SendRpcCommandRequest>(gameStartRpc);
} }
// Apply all queued entity modifications.
entityCommandBuffer.Playback(state.EntityManager); entityCommandBuffer.Playback(state.EntityManager);
// Update the singleton `PlayerCounter` component with the new value.
SystemAPI.SetSingleton(playerCounter); SystemAPI.SetSingleton(playerCounter);
} }
} }
\ No newline at end of file
...@@ -5,12 +5,17 @@ using Unity.NetCode; ...@@ -5,12 +5,17 @@ using Unity.NetCode;
using Unity.Transforms; using Unity.Transforms;
using UnityEngine; using UnityEngine;
[UpdateInGroup(typeof(PredictedSimulationSystemGroup), OrderLast = true)] [UpdateInGroup(typeof(PredictedSimulationSystemGroup), OrderLast = true)]
public partial struct DestroyEntitySystem : ISystem public partial struct DestroyEntitySystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when the required components
/// and entities are present in the world.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
// Require specific components and entities for the system to update.
state.RequireForUpdate<RespawnEntityTag>(); state.RequireForUpdate<RespawnEntityTag>();
state.RequireForUpdate<BeginSimulationEntityCommandBufferSystem.Singleton>(); state.RequireForUpdate<BeginSimulationEntityCommandBufferSystem.Singleton>();
state.RequireForUpdate<NetworkTime>(); state.RequireForUpdate<NetworkTime>();
...@@ -18,35 +23,49 @@ public partial struct DestroyEntitySystem : ISystem ...@@ -18,35 +23,49 @@ public partial struct DestroyEntitySystem : ISystem
state.RequireForUpdate<DestroyEntityTag>(); state.RequireForUpdate<DestroyEntityTag>();
} }
/// <summary>
/// Called every frame to process entities marked for destruction. Handles player-specific logic
/// such as respawn scheduling and destroys non-player entities directly.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
// Retrieve the current network time.
var networkTime = SystemAPI.GetSingleton<NetworkTime>(); var networkTime = SystemAPI.GetSingleton<NetworkTime>();
// Skip processing if this is not the first time fully predicting the current tick.
if (!networkTime.IsFirstTimeFullyPredictingTick) return; if (!networkTime.IsFirstTimeFullyPredictingTick) return;
// Get the current server tick.
var currentTick = networkTime.ServerTick; var currentTick = networkTime.ServerTick;
// Retrieve the EntityCommandBuffer for queuing entity modifications.
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>(); var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged); var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
// Iterate through all entities with a LocalTransform component, DestroyEntityTag, and Simulate tag.
foreach (var (transform, entity) in SystemAPI.Query<RefRW<LocalTransform>>() foreach (var (transform, entity) in SystemAPI.Query<RefRW<LocalTransform>>()
.WithAll<DestroyEntityTag, Simulate>().WithEntityAccess()) .WithAll<DestroyEntityTag, Simulate>().WithEntityAccess())
{ {
// Check if the current world is the server.
if (state.World.IsServer()) if (state.World.IsServer())
{ {
// Handle player entities.
if (SystemAPI.HasComponent<PlayerTag>(entity)) if (SystemAPI.HasComponent<PlayerTag>(entity))
{ {
// Retrieve the network entity and respawn-related components.
var networkEntity = SystemAPI.GetComponent<NetworkEntityReference>(entity).Value; var networkEntity = SystemAPI.GetComponent<NetworkEntityReference>(entity).Value;
var respawnEntity = SystemAPI.GetSingletonEntity<RespawnEntityTag>(); var respawnEntity = SystemAPI.GetSingletonEntity<RespawnEntityTag>();
var respawnTickCount = SystemAPI.GetComponent<RespawnTickCount>(respawnEntity).Value; var respawnTickCount = SystemAPI.GetComponent<RespawnTickCount>(respawnEntity).Value;
// Calculate the tick at which the player should respawn.
var respawnTick = currentTick; var respawnTick = currentTick;
respawnTick.Add(respawnTickCount); respawnTick.Add(respawnTickCount);
// Store NetworkId BEFORE destroying entity or networkEntity // Store the NetworkId before destroying the entity.
int networkIdValue = SystemAPI.GetComponent<NetworkId>(networkEntity).Value; int networkIdValue = SystemAPI.GetComponent<NetworkId>(networkEntity).Value;
// Append the player to the respawn buffer with the calculated respawn tick.
ecb.AppendToBuffer(respawnEntity, new RespawnBufferElement ecb.AppendToBuffer(respawnEntity, new RespawnBufferElement
{ {
NetworkEntity = networkEntity, NetworkEntity = networkEntity,
...@@ -54,11 +73,13 @@ public partial struct DestroyEntitySystem : ISystem ...@@ -54,11 +73,13 @@ public partial struct DestroyEntitySystem : ISystem
NetworkId = networkIdValue NetworkId = networkIdValue
}); });
// Destroy the player entity.
ecb.DestroyEntity(entity); ecb.DestroyEntity(entity);
} }
else else
{ {
ecb.DestroyEntity(entity); // Destroy non-player entities // Destroy non-player entities directly.
ecb.DestroyEntity(entity);
} }
} }
} }
......
...@@ -5,27 +5,44 @@ using Unity.NetCode; ...@@ -5,27 +5,44 @@ using Unity.NetCode;
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))] [UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
public partial struct DestroyOnTimerSystem : ISystem public partial struct DestroyOnTimerSystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when the required components
/// and entities are present in the world.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
// Require specific components and entities for the system to update.
state.RequireForUpdate<NetworkTime>(); state.RequireForUpdate<NetworkTime>();
state.RequireForUpdate<EndSimulationEntityCommandBufferSystem.Singleton>(); state.RequireForUpdate<EndSimulationEntityCommandBufferSystem.Singleton>();
state.RequireForUpdate<DestroyAtTick>(); state.RequireForUpdate<DestroyAtTick>();
state.RequireForUpdate<GamePlayingTag>(); state.RequireForUpdate<GamePlayingTag>();
} }
/// <summary>
/// Called every frame to process entities with a `DestroyAtTick` component. Adds a `DestroyEntityTag`
/// to entities whose destruction tick has been reached or passed.
/// </summary>
/// <param name="state">The current state of the system.</param>
[BurstCompile] [BurstCompile]
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
// Retrieve the EntityCommandBuffer for queuing entity modifications.
var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>(); var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged); var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
// Get the current server tick.
var currentTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick; var currentTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
// Iterate through all entities with a `DestroyAtTick` component and a `Simulate` tag,
// but without a `DestroyEntityTag`.
foreach (var (destroyAtTick, entity) in SystemAPI.Query<RefRW<DestroyAtTick>>().WithAll<Simulate>() foreach (var (destroyAtTick, entity) in SystemAPI.Query<RefRW<DestroyAtTick>>().WithAll<Simulate>()
.WithNone<DestroyEntityTag>().WithEntityAccess()) .WithNone<DestroyEntityTag>().WithEntityAccess())
{ {
// Check if the current tick is equal to or newer than the destruction tick.
if (currentTick.Equals(destroyAtTick.ValueRW.Value) || currentTick.IsNewerThan(destroyAtTick.ValueRW.Value)) if (currentTick.Equals(destroyAtTick.ValueRW.Value) || currentTick.IsNewerThan(destroyAtTick.ValueRW.Value))
{ {
// Add the `DestroyEntityTag` to mark the entity for destruction.
ecb.AddComponent<DestroyEntityTag>(entity); ecb.AddComponent<DestroyEntityTag>(entity);
} }
} }
......
...@@ -2,30 +2,56 @@ using Unity.Collections; ...@@ -2,30 +2,56 @@ using Unity.Collections;
using Unity.Entities; using Unity.Entities;
using Unity.NetCode; using Unity.NetCode;
/// <summary>
/// System responsible for initializing the destruction timer for entities.
/// This system calculates the tick at which entities should be destroyed based on their lifetime
/// and adds a `DestroyAtTick` component to mark the destruction time.
/// </summary>
public partial struct InitializeDestroyOnTimerSystem : ISystem public partial struct InitializeDestroyOnTimerSystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when the required components
/// and entities are present in the world.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
// Require specific components and entities for the system to update.
state.RequireForUpdate<NetworkTime>(); state.RequireForUpdate<NetworkTime>();
state.RequireForUpdate<DestroyOnTimer>(); state.RequireForUpdate<DestroyOnTimer>();
state.RequireForUpdate<GamePlayingTag>(); state.RequireForUpdate<GamePlayingTag>();
} }
/// <summary>
/// Called every frame to process entities with a `DestroyOnTimer` component. Calculates the destruction tick
/// based on the entity's lifetime and adds a `DestroyAtTick` component to schedule its destruction.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
// Create a temporary EntityCommandBuffer to queue entity modifications.
var ecb = new EntityCommandBuffer(Allocator.Temp); var ecb = new EntityCommandBuffer(Allocator.Temp);
// Retrieve the simulation tick rate and the current server tick.
var simulationTickRate = NetCodeConfig.Global.ClientServerTickRate.SimulationTickRate; var simulationTickRate = NetCodeConfig.Global.ClientServerTickRate.SimulationTickRate;
var currentTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick; var currentTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
// Iterate through all entities with a `DestroyOnTimer` component but without a `DestroyAtTick` component.
foreach (var (destroyOnTimer, entity) in SystemAPI.Query<RefRW<DestroyOnTimer>>().WithNone<DestroyAtTick>() foreach (var (destroyOnTimer, entity) in SystemAPI.Query<RefRW<DestroyOnTimer>>().WithNone<DestroyAtTick>()
.WithEntityAccess()) .WithEntityAccess())
{ {
// Calculate the lifetime in ticks based on the entity's lifetime in seconds.
var lifetimeInTicks = (uint)(destroyOnTimer.ValueRW.Value * simulationTickRate); var lifetimeInTicks = (uint)(destroyOnTimer.ValueRW.Value * simulationTickRate);
// Calculate the target tick at which the entity should be destroyed.
var targetTick = currentTick; var targetTick = currentTick;
targetTick.Add(lifetimeInTicks); targetTick.Add(lifetimeInTicks);
// Add the `DestroyAtTick` component to the entity with the calculated destruction tick.
ecb.AddComponent(entity, new DestroyAtTick { Value = targetTick }); ecb.AddComponent(entity, new DestroyAtTick { Value = targetTick });
} }
// Apply all queued entity modifications.
ecb.Playback(state.EntityManager); ecb.Playback(state.EntityManager);
} }
} }
\ No newline at end of file
...@@ -10,9 +10,20 @@ using UnityEngine; ...@@ -10,9 +10,20 @@ using UnityEngine;
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))] [UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
public partial class RespawnPlayerSystem : SystemBase public partial class RespawnPlayerSystem : SystemBase
{ {
/// <summary>
/// Action invoked to update the respawn countdown with the number of seconds remaining until the player respawns.
/// </summary>
public Action<int> OnUpdateRespawnCountdown; public Action<int> OnUpdateRespawnCountdown;
/// <summary>
/// Action invoked when the player respawns.
/// </summary>
public Action OnRespawn; public Action OnRespawn;
/// <summary>
/// Called when the system is created. Ensures the system only updates when the required components
/// and entities are present in the world.
/// </summary>
protected override void OnCreate() protected override void OnCreate()
{ {
RequireForUpdate<NetworkTime>(); RequireForUpdate<NetworkTime>();
...@@ -20,6 +31,9 @@ public partial class RespawnPlayerSystem : SystemBase ...@@ -20,6 +31,9 @@ public partial class RespawnPlayerSystem : SystemBase
RequireForUpdate<GamePlayingTag>(); RequireForUpdate<GamePlayingTag>();
} }
/// <summary>
/// Called when the system starts running. Instantiates the respawn entity if it does not already exist.
/// </summary>
protected override void OnStartRunning() protected override void OnStartRunning()
{ {
if (SystemAPI.HasSingleton<RespawnEntityTag>()) return; if (SystemAPI.HasSingleton<RespawnEntityTag>()) return;
...@@ -27,6 +41,10 @@ public partial class RespawnPlayerSystem : SystemBase ...@@ -27,6 +41,10 @@ public partial class RespawnPlayerSystem : SystemBase
EntityManager.Instantiate(respawnPrefab); EntityManager.Instantiate(respawnPrefab);
} }
/// <summary>
/// Called every frame to process player respawn logic. Handles server-side player instantiation
/// and client-side countdown updates.
/// </summary>
protected override void OnUpdate() protected override void OnUpdate()
{ {
var networkTime = SystemAPI.GetSingleton<NetworkTime>(); var networkTime = SystemAPI.GetSingleton<NetworkTime>();
...@@ -37,6 +55,7 @@ public partial class RespawnPlayerSystem : SystemBase ...@@ -37,6 +55,7 @@ public partial class RespawnPlayerSystem : SystemBase
var ecb = new EntityCommandBuffer(Allocator.Temp); var ecb = new EntityCommandBuffer(Allocator.Temp);
// Iterate through all entities with a RespawnBufferElement buffer.
foreach (var respawnBuffer in SystemAPI.Query<DynamicBuffer<RespawnBufferElement>>() foreach (var respawnBuffer in SystemAPI.Query<DynamicBuffer<RespawnBufferElement>>()
.WithAll<RespawnTickCount, Simulate>()) .WithAll<RespawnTickCount, Simulate>())
{ {
...@@ -46,10 +65,12 @@ public partial class RespawnPlayerSystem : SystemBase ...@@ -46,10 +65,12 @@ public partial class RespawnPlayerSystem : SystemBase
{ {
var curRespawn = respawnBuffer[i]; var curRespawn = respawnBuffer[i];
// Check if the current tick matches or exceeds the respawn tick.
if (currentTick.Equals(curRespawn.RespawnTick) || currentTick.IsNewerThan(curRespawn.RespawnTick)) if (currentTick.Equals(curRespawn.RespawnTick) || currentTick.IsNewerThan(curRespawn.RespawnTick))
{ {
if (isServer) if (isServer)
{ {
// Server-side logic: Instantiate a new player entity and set its components.
int networkId = SystemAPI.GetComponent<NetworkId>(curRespawn.NetworkEntity).Value; int networkId = SystemAPI.GetComponent<NetworkId>(curRespawn.NetworkEntity).Value;
Entity playerPrefab = SystemAPI.GetSingleton<EntititesReferences>().PlayerPrefabEntity; Entity playerPrefab = SystemAPI.GetSingleton<EntititesReferences>().PlayerPrefabEntity;
...@@ -65,6 +86,7 @@ public partial class RespawnPlayerSystem : SystemBase ...@@ -65,6 +86,7 @@ public partial class RespawnPlayerSystem : SystemBase
} }
else else
{ {
// Client-side logic: Invoke the respawn action and create a camera for the new player.
OnRespawn?.Invoke(); OnRespawn?.Invoke();
if (SystemAPI.TryGetSingleton<NetworkId>(out var clientNetworkId) && if (SystemAPI.TryGetSingleton<NetworkId>(out var clientNetworkId) &&
curRespawn.NetworkId == clientNetworkId.Value) curRespawn.NetworkId == clientNetworkId.Value)
...@@ -75,6 +97,7 @@ public partial class RespawnPlayerSystem : SystemBase ...@@ -75,6 +97,7 @@ public partial class RespawnPlayerSystem : SystemBase
} }
else if (!isServer) else if (!isServer)
{ {
// Client-side logic: Update the respawn countdown.
if (SystemAPI.TryGetSingleton<NetworkId>(out var networkId)) if (SystemAPI.TryGetSingleton<NetworkId>(out var networkId))
{ {
if (networkId.Value == curRespawn.NetworkId) if (networkId.Value == curRespawn.NetworkId)
...@@ -89,15 +112,21 @@ public partial class RespawnPlayerSystem : SystemBase ...@@ -89,15 +112,21 @@ public partial class RespawnPlayerSystem : SystemBase
} }
} }
// Remove processed respawn entries from the buffer.
foreach (var respawnIndex in respawnsToCleanup) foreach (var respawnIndex in respawnsToCleanup)
{ {
respawnBuffer.RemoveAt(respawnIndex); respawnBuffer.RemoveAt(respawnIndex);
} }
} }
// Apply all queued entity modifications.
ecb.Playback(EntityManager); ecb.Playback(EntityManager);
} }
/// <summary>
/// Creates a camera for the newly respawned player.
/// </summary>
/// <param name="networkId">The network ID of the player.</param>
private void CreateCameraForNewPlayer(int networkId) private void CreateCameraForNewPlayer(int networkId)
{ {
GameObject playerCameraGO = new GameObject($"Camera{networkId}"); GameObject playerCameraGO = new GameObject($"Camera{networkId}");
......
...@@ -2,55 +2,100 @@ using Unity.Entities; ...@@ -2,55 +2,100 @@ using Unity.Entities;
using Unity.Entities.UniversalDelegates; using Unity.Entities.UniversalDelegates;
using UnityEngine; using UnityEngine;
/// <summary>
/// Represents an aspect for managing enemy spawning logic, including spawn timers,
/// spawn counters, and cooldowns for different enemy types.
/// </summary>
public readonly partial struct EnemySpawnerAspect : IAspect public readonly partial struct EnemySpawnerAspect : IAspect
{ {
// Reference to the enemy spawn timer component.
private readonly RefRW<EnemySpawnTimer> _enemySpawnTimer; private readonly RefRW<EnemySpawnTimer> _enemySpawnTimer;
// Reference to the game start properties component.
private readonly RefRO<GameStartProperties> _gameStartProperties; private readonly RefRO<GameStartProperties> _gameStartProperties;
// Reference to the spawnable enemies counter component.
private readonly RefRW<SpawnableEnemiesCounter> _spawnableEnemiesCounter; private readonly RefRW<SpawnableEnemiesCounter> _spawnableEnemiesCounter;
/// <summary>
/// Gets or sets the time remaining for the next slime enemy spawn.
/// </summary>
public float TimeForNextSlimeSpawn public float TimeForNextSlimeSpawn
{ {
get => _enemySpawnTimer.ValueRO.SlimeSpawnTimer; get => _enemySpawnTimer.ValueRO.SlimeSpawnTimer;
set => _enemySpawnTimer.ValueRW.SlimeSpawnTimer = value; set => _enemySpawnTimer.ValueRW.SlimeSpawnTimer = value;
} }
/// <summary>
/// Gets or sets the time remaining for the next rogue enemy spawn.
/// </summary>
public float TimeForNextRogueSpawn public float TimeForNextRogueSpawn
{ {
get => _enemySpawnTimer.ValueRO.RogueSpawnTimer; get => _enemySpawnTimer.ValueRO.RogueSpawnTimer;
set => _enemySpawnTimer.ValueRW.RogueSpawnTimer = value; set => _enemySpawnTimer.ValueRW.RogueSpawnTimer = value;
} }
/// <summary>
/// Gets or sets the random spawn offset used to vary spawn timings.
/// </summary>
public float RandomSpawnOffset public float RandomSpawnOffset
{ {
get => _enemySpawnTimer.ValueRO.RandomSpawnOffset; get => _enemySpawnTimer.ValueRO.RandomSpawnOffset;
set => _enemySpawnTimer.ValueRW.RandomSpawnOffset = value; set => _enemySpawnTimer.ValueRW.RandomSpawnOffset = value;
} }
/// <summary>
/// Gets the total number of rogue enemies to spawn, as defined in the game start properties.
/// </summary>
public int RogueSpawnAmount => _gameStartProperties.ValueRO.RogueEnemyAmount; public int RogueSpawnAmount => _gameStartProperties.ValueRO.RogueEnemyAmount;
/// <summary>
/// Gets the total number of slime enemies to spawn, as defined in the game start properties.
/// </summary>
public int SlimeSpawnAmount => _gameStartProperties.ValueRO.SlimeEnemyAmount; public int SlimeSpawnAmount => _gameStartProperties.ValueRO.SlimeEnemyAmount;
/// <summary>
/// Gets or sets the current count of spawned rogue enemies.
/// </summary>
public int RogueEnemyCounter public int RogueEnemyCounter
{ {
get => _spawnableEnemiesCounter.ValueRO.RogueEnemyCounter; get => _spawnableEnemiesCounter.ValueRO.RogueEnemyCounter;
set => _spawnableEnemiesCounter.ValueRW.RogueEnemyCounter = value; set => _spawnableEnemiesCounter.ValueRW.RogueEnemyCounter = value;
} }
/// <summary>
/// Gets or sets the current count of spawned slime enemies.
/// </summary>
public int SlimeEnemyCounter public int SlimeEnemyCounter
{ {
get => _spawnableEnemiesCounter.ValueRO.SlimeEnemyCounter; get => _spawnableEnemiesCounter.ValueRO.SlimeEnemyCounter;
set => _spawnableEnemiesCounter.ValueRW.SlimeEnemyCounter = value; set => _spawnableEnemiesCounter.ValueRW.SlimeEnemyCounter = value;
} }
/// <summary>
/// Gets the cooldown duration for spawning slime enemies.
/// </summary>
public float SlimeSpawnCoolDown => _enemySpawnTimer.ValueRO.SlimeSpawnCooldown; public float SlimeSpawnCoolDown => _enemySpawnTimer.ValueRO.SlimeSpawnCooldown;
/// <summary>
/// Gets the cooldown duration for spawning rogue enemies.
/// </summary>
public float RogueSpawnCoolDown => _enemySpawnTimer.ValueRO.RogueSpawnCooldown; public float RogueSpawnCoolDown => _enemySpawnTimer.ValueRO.RogueSpawnCooldown;
/// <summary>
/// Determines whether a slime enemy can spawn based on the timer and spawn limits.
/// </summary>
public bool CanEnemySlimeSpawn => TimeForNextSlimeSpawn <= 0 && SlimeSpawnAmount > SlimeEnemyCounter; public bool CanEnemySlimeSpawn => TimeForNextSlimeSpawn <= 0 && SlimeSpawnAmount > SlimeEnemyCounter;
public bool CanEnemyRogueSpawn => TimeForNextRogueSpawn <= 0 && RogueSpawnAmount > RogueEnemyCounter;
/// <summary>
/// Determines whether a rogue enemy can spawn based on the timer and spawn limits.
/// </summary>
public bool CanEnemyRogueSpawn => TimeForNextRogueSpawn <= 0 && RogueSpawnAmount > RogueEnemyCounter;
/// <summary>
/// Decrements the spawn timers and adjusts the random spawn offset over time.
/// </summary>
/// <param name="deltaTime">The time elapsed since the last update.</param>
public void DecrementTimers(float deltaTime) public void DecrementTimers(float deltaTime)
{ {
TimeForNextSlimeSpawn -= deltaTime; TimeForNextSlimeSpawn -= deltaTime;
...@@ -61,21 +106,33 @@ public readonly partial struct EnemySpawnerAspect : IAspect ...@@ -61,21 +106,33 @@ public readonly partial struct EnemySpawnerAspect : IAspect
RandomSpawnOffset = 5f; RandomSpawnOffset = 5f;
} }
/// <summary>
/// Increments the counter for spawned slime enemies.
/// </summary>
public void IncreaseSlimeCounter() public void IncreaseSlimeCounter()
{ {
SlimeEnemyCounter++; SlimeEnemyCounter++;
} }
/// <summary>
/// Increments the counter for spawned rogue enemies.
/// </summary>
public void IncreaseRogueCounter() public void IncreaseRogueCounter()
{ {
RogueEnemyCounter++; RogueEnemyCounter++;
} }
/// <summary>
/// Resets the timer for spawning rogue enemies to the cooldown duration.
/// </summary>
public void ResetRoqueTimer() public void ResetRoqueTimer()
{ {
TimeForNextRogueSpawn = RogueSpawnCoolDown; TimeForNextRogueSpawn = RogueSpawnCoolDown;
} }
/// <summary>
/// Resets the timer for spawning slime enemies to the cooldown duration.
/// </summary>
public void ResetSlimeTimer() public void ResetSlimeTimer()
{ {
TimeForNextSlimeSpawn = SlimeSpawnCoolDown; TimeForNextSlimeSpawn = SlimeSpawnCoolDown;
......
...@@ -5,11 +5,22 @@ using Unity.Physics; ...@@ -5,11 +5,22 @@ using Unity.Physics;
using Unity.Transforms; using Unity.Transforms;
using UnityEngine; using UnityEngine;
/// <summary>
/// System responsible for spawning enemies during the game.
/// Handles the logic for decrementing spawn timers, checking spawn conditions,
/// and instantiating enemies at designated spawn points.
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
partial struct EnemySpawnerSystem : ISystem partial struct EnemySpawnerSystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when the required components
/// and entities are present in the world.
/// </summary>
/// <param name="state">The current state of the system.</param>
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
// Require specific components and entities for the system to update.
state.RequireForUpdate<GamePlayingTag>(); state.RequireForUpdate<GamePlayingTag>();
state.RequireForUpdate<EnemySpawnPoints>(); state.RequireForUpdate<EnemySpawnPoints>();
state.RequireForUpdate<EnemySpawnTimer>(); state.RequireForUpdate<EnemySpawnTimer>();
...@@ -17,17 +28,27 @@ partial struct EnemySpawnerSystem : ISystem ...@@ -17,17 +28,27 @@ partial struct EnemySpawnerSystem : ISystem
state.RequireForUpdate<BeginSimulationEntityCommandBufferSystem.Singleton>(); state.RequireForUpdate<BeginSimulationEntityCommandBufferSystem.Singleton>();
} }
/// <summary>
/// Called every frame to process enemy spawning logic.
/// Decrements spawn timers, checks if enemies can spawn, and spawns them at designated positions.
/// </summary>
/// <param name="state">The current state of the system.</param>
[BurstCompile] [BurstCompile]
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
// Retrieve the time elapsed since the last frame.
var deltaTime = SystemAPI.Time.DeltaTime; var deltaTime = SystemAPI.Time.DeltaTime;
// Iterate through all entities with the EnemySpawnerAspect.
foreach (EnemySpawnerAspect aspect in SystemAPI.Query<EnemySpawnerAspect>()) foreach (EnemySpawnerAspect aspect in SystemAPI.Query<EnemySpawnerAspect>())
{ {
// Decrement spawn timers for the current aspect.
aspect.DecrementTimers(deltaTime); aspect.DecrementTimers(deltaTime);
// Check if a slime enemy can spawn.
if (aspect.CanEnemySlimeSpawn) if (aspect.CanEnemySlimeSpawn)
{ {
// Retrieve spawn points and the command buffer for entity modifications.
Entity enemyPropertiesEntity = SystemAPI.GetSingletonEntity<EnemySpawnPoints>(); Entity enemyPropertiesEntity = SystemAPI.GetSingletonEntity<EnemySpawnPoints>();
DynamicBuffer<EnemySpawnPoints> spawnPoints = DynamicBuffer<EnemySpawnPoints> spawnPoints =
SystemAPI.GetBuffer<EnemySpawnPoints>(enemyPropertiesEntity); SystemAPI.GetBuffer<EnemySpawnPoints>(enemyPropertiesEntity);
...@@ -35,17 +56,22 @@ partial struct EnemySpawnerSystem : ISystem ...@@ -35,17 +56,22 @@ partial struct EnemySpawnerSystem : ISystem
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged); var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
Entity enemySlimeEntity = SystemAPI.GetSingleton<EntititesReferences>().SlimeEnemyEntity; Entity enemySlimeEntity = SystemAPI.GetSingleton<EntititesReferences>().SlimeEnemyEntity;
// Calculate the spawn position for the slime enemy.
int slimeSpawnIndex = aspect.SlimeEnemyCounter % spawnPoints.Length; int slimeSpawnIndex = aspect.SlimeEnemyCounter % spawnPoints.Length;
float randomValue = aspect.RandomSpawnOffset; float randomValue = aspect.RandomSpawnOffset;
float3 spawnPosition = float3 spawnPosition =
spawnPoints[slimeSpawnIndex].SpawnPoint + new float3(randomValue, 0, -randomValue); spawnPoints[slimeSpawnIndex].SpawnPoint + new float3(randomValue, 0, -randomValue);
// Spawn the slime enemy and update the aspect's counters and timers.
SpawnEnemy(ecb, enemySlimeEntity, spawnPosition); SpawnEnemy(ecb, enemySlimeEntity, spawnPosition);
aspect.IncreaseSlimeCounter(); aspect.IncreaseSlimeCounter();
aspect.ResetSlimeTimer(); aspect.ResetSlimeTimer();
} }
// Check if a rogue enemy can spawn.
if (aspect.CanEnemyRogueSpawn) if (aspect.CanEnemyRogueSpawn)
{ {
// Retrieve spawn points and the command buffer for entity modifications.
Entity enemyPropertiesEntity = SystemAPI.GetSingletonEntity<EnemySpawnPoints>(); Entity enemyPropertiesEntity = SystemAPI.GetSingletonEntity<EnemySpawnPoints>();
DynamicBuffer<EnemySpawnPoints> spawnPoints = DynamicBuffer<EnemySpawnPoints> spawnPoints =
SystemAPI.GetBuffer<EnemySpawnPoints>(enemyPropertiesEntity); SystemAPI.GetBuffer<EnemySpawnPoints>(enemyPropertiesEntity);
...@@ -53,10 +79,13 @@ partial struct EnemySpawnerSystem : ISystem ...@@ -53,10 +79,13 @@ partial struct EnemySpawnerSystem : ISystem
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged); var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
Entity enemyRogueEntity = SystemAPI.GetSingleton<EntititesReferences>().RougeEnemyEntity; Entity enemyRogueEntity = SystemAPI.GetSingleton<EntititesReferences>().RougeEnemyEntity;
// Calculate the spawn position for the rogue enemy.
int rogueSpawnIndex = aspect.RogueEnemyCounter % spawnPoints.Length; int rogueSpawnIndex = aspect.RogueEnemyCounter % spawnPoints.Length;
float randomValue = aspect.RandomSpawnOffset; float randomValue = aspect.RandomSpawnOffset;
float3 spawnPosition = float3 spawnPosition =
spawnPoints[rogueSpawnIndex].SpawnPoint + new float3(randomValue, 0, -randomValue); spawnPoints[rogueSpawnIndex].SpawnPoint + new float3(randomValue, 0, -randomValue);
// Spawn the rogue enemy and update the aspect's counters and timers.
SpawnEnemy(ecb, enemyRogueEntity, spawnPosition); SpawnEnemy(ecb, enemyRogueEntity, spawnPosition);
aspect.IncreaseRogueCounter(); aspect.IncreaseRogueCounter();
aspect.ResetRoqueTimer(); aspect.ResetRoqueTimer();
...@@ -64,10 +93,16 @@ partial struct EnemySpawnerSystem : ISystem ...@@ -64,10 +93,16 @@ partial struct EnemySpawnerSystem : ISystem
} }
} }
/// <summary>
/// Spawns an enemy entity at the specified position.
/// </summary>
/// <param name="ecb">The EntityCommandBuffer used to queue entity modifications.</param>
/// <param name="spawnableEntity">The entity prefab to spawn.</param>
/// <param name="position">The position where the entity should be spawned.</param>
void SpawnEnemy(EntityCommandBuffer ecb, Entity spawnableEntity, float3 position) void SpawnEnemy(EntityCommandBuffer ecb, Entity spawnableEntity, float3 position)
{ {
// Instantiate the enemy entity and set its position.
Entity newEnemy = ecb.Instantiate(spawnableEntity); Entity newEnemy = ecb.Instantiate(spawnableEntity);
ecb.SetComponent(newEnemy, ecb.SetComponent(newEnemy, LocalTransform.FromPosition(position));
LocalTransform.FromPosition(position));
} }
} }
\ No newline at end of file
...@@ -5,32 +5,46 @@ using Unity.Mathematics; ...@@ -5,32 +5,46 @@ using Unity.Mathematics;
using Unity.NetCode; using Unity.NetCode;
using UnityEngine; using UnityEngine;
[UpdateInGroup(typeof(GhostInputSystemGroup))] [UpdateInGroup(typeof(GhostInputSystemGroup))]
public partial class NetcodePlayerInputSystem : SystemBase public partial class NetcodePlayerInputSystem : SystemBase
{ {
private InputSystem_Actions _inputActions; private InputSystem_Actions _inputActions;
/// <summary>
/// Called when the system is created. Initializes the input actions and ensures the system
/// only updates when the required components and entities are present in the world.
/// </summary>
protected override void OnCreate() protected override void OnCreate()
{ {
// Initialize and enable the input actions.
_inputActions = new InputSystem_Actions(); _inputActions = new InputSystem_Actions();
_inputActions.Enable(); _inputActions.Enable();
// Require specific components and entities for the system to update.
RequireForUpdate<NetworkStreamInGame>(); RequireForUpdate<NetworkStreamInGame>();
RequireForUpdate<GamePlayingTag>(); RequireForUpdate<GamePlayingTag>();
RequireForUpdate<NetcodePlayerInput>(); RequireForUpdate<NetcodePlayerInput>();
} }
/// <summary>
/// Called every frame to process player input. Updates the `NetcodePlayerInput` component
/// with the player's movement vector and sprinting state.
/// </summary>
protected override void OnUpdate() protected override void OnUpdate()
{ {
// Iterate through all entities with a `NetcodePlayerInput` component and a `GhostOwnerIsLocal` tag.
foreach (RefRW<NetcodePlayerInput> netcodePlayerInput in SystemAPI.Query<RefRW<NetcodePlayerInput>>() foreach (RefRW<NetcodePlayerInput> netcodePlayerInput in SystemAPI.Query<RefRW<NetcodePlayerInput>>()
.WithAll<GhostOwnerIsLocal>()) .WithAll<GhostOwnerIsLocal>())
{ {
// Update the input vector and sprinting state from the input actions.
netcodePlayerInput.ValueRW.inputVector = _inputActions.Player.Move.ReadValue<Vector2>(); netcodePlayerInput.ValueRW.inputVector = _inputActions.Player.Move.ReadValue<Vector2>();
netcodePlayerInput.ValueRW.isSprinting = _inputActions.Player.Sprint.IsPressed(); netcodePlayerInput.ValueRW.isSprinting = _inputActions.Player.Sprint.IsPressed();
} }
} }
/// <summary>
/// Called when the system is destroyed. Disables the input actions to clean up resources.
/// </summary>
protected override void OnDestroy() protected override void OnDestroy()
{ {
base.OnDestroy(); base.OnDestroy();
......
...@@ -6,12 +6,23 @@ using Unity.Physics; ...@@ -6,12 +6,23 @@ using Unity.Physics;
using Unity.Transforms; using Unity.Transforms;
using UnityEngine; using UnityEngine;
/// <summary>
/// System responsible for handling player movement in a netcode environment.
/// This system processes player input, updates physics velocity, and adjusts player rotation
/// based on movement direction and sprinting state.
/// </summary>
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))] [UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
partial struct NetcodePlayerMovementSystem : ISystem partial struct NetcodePlayerMovementSystem : ISystem
{ {
/// <summary>
/// Called when the system is created. Ensures the system only updates when the required components
/// and entities are present in the world.
/// </summary>
/// <param name="state">The current state of the system.</param>
[BurstCompile] [BurstCompile]
public void OnCreate(ref SystemState state) public void OnCreate(ref SystemState state)
{ {
// Require specific components and entities for the system to update.
state.RequireForUpdate<NetcodePlayerInput>(); state.RequireForUpdate<NetcodePlayerInput>();
state.RequireForUpdate<PhysicsVelocity>(); state.RequireForUpdate<PhysicsVelocity>();
state.RequireForUpdate<LocalTransform>(); state.RequireForUpdate<LocalTransform>();
...@@ -19,11 +30,18 @@ partial struct NetcodePlayerMovementSystem : ISystem ...@@ -19,11 +30,18 @@ partial struct NetcodePlayerMovementSystem : ISystem
state.RequireForUpdate<GamePlayingTag>(); state.RequireForUpdate<GamePlayingTag>();
} }
/// <summary>
/// Called every frame to process player movement logic.
/// Updates the player's velocity and rotation based on input and sprinting state.
/// </summary>
/// <param name="state">The current state of the system.</param>
[BurstCompile] [BurstCompile]
public void OnUpdate(ref SystemState state) public void OnUpdate(ref SystemState state)
{ {
// Retrieve the time elapsed since the last frame.
float deltaTime = SystemAPI.Time.DeltaTime; float deltaTime = SystemAPI.Time.DeltaTime;
// Iterate through all entities with the required components.
foreach ((RefRO<NetcodePlayerInput> netcodePlayerInput, RefRW<PhysicsVelocity> physicsVelocity, foreach ((RefRO<NetcodePlayerInput> netcodePlayerInput, RefRW<PhysicsVelocity> physicsVelocity,
RefRW<LocalTransform> localTransform, RefRW<PlayerSprintData> sprintData) RefRW<LocalTransform> localTransform, RefRW<PlayerSprintData> sprintData)
in SystemAPI in SystemAPI
...@@ -31,13 +49,17 @@ partial struct NetcodePlayerMovementSystem : ISystem ...@@ -31,13 +49,17 @@ partial struct NetcodePlayerMovementSystem : ISystem
RefRW<PlayerSprintData>>() RefRW<PlayerSprintData>>()
.WithAll<Simulate>()) .WithAll<Simulate>())
{ {
// Calculate the movement vector based on player input.
float3 moveVector = new float3(netcodePlayerInput.ValueRO.inputVector.x, 0, float3 moveVector = new float3(netcodePlayerInput.ValueRO.inputVector.x, 0,
netcodePlayerInput.ValueRO.inputVector.y); netcodePlayerInput.ValueRO.inputVector.y);
// Set the default movement speed to walking speed.
float moveSpeed = sprintData.ValueRO.walkSpeed; float moveSpeed = sprintData.ValueRO.walkSpeed;
// Check if the player is sprinting.
if (netcodePlayerInput.ValueRO.isSprinting) if (netcodePlayerInput.ValueRO.isSprinting)
{ {
// Handle sprinting logic and cooldowns.
if (!sprintData.ValueRO.isSprintCooldown) if (!sprintData.ValueRO.isSprintCooldown)
{ {
sprintData.ValueRW.sprintRemaining -= deltaTime; sprintData.ValueRW.sprintRemaining -= deltaTime;
...@@ -53,6 +75,7 @@ partial struct NetcodePlayerMovementSystem : ISystem ...@@ -53,6 +75,7 @@ partial struct NetcodePlayerMovementSystem : ISystem
} }
} }
// Handle sprint cooldown logic.
if (sprintData.ValueRO.isSprintCooldown) if (sprintData.ValueRO.isSprintCooldown)
{ {
sprintData.ValueRW.sprintCooldown -= deltaTime; sprintData.ValueRW.sprintCooldown -= deltaTime;
...@@ -62,10 +85,11 @@ partial struct NetcodePlayerMovementSystem : ISystem ...@@ -62,10 +85,11 @@ partial struct NetcodePlayerMovementSystem : ISystem
} }
} }
// Smoothly update the player's velocity based on the movement vector and speed.
physicsVelocity.ValueRW.Linear = physicsVelocity.ValueRW.Linear =
math.lerp(physicsVelocity.ValueRO.Linear, moveVector * moveSpeed, deltaTime * 10); math.lerp(physicsVelocity.ValueRO.Linear, moveVector * moveSpeed, deltaTime * 10);
// Update the player's rotation to face the movement direction if moving.
if (!math.all(moveVector == float3.zero)) if (!math.all(moveVector == float3.zero))
{ {
quaternion targetRotation = quaternion.LookRotationSafe(moveVector, math.up()); quaternion targetRotation = quaternion.LookRotationSafe(moveVector, math.up());
......
...@@ -222,7 +222,7 @@ MonoBehaviour: ...@@ -222,7 +222,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 7e19d579a4586dd40805276eb2d1a684, type: 3} m_Script: {fileID: 11500000, guid: 7e19d579a4586dd40805276eb2d1a684, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
DestroyOnTimer: 2 DestroyOnTimer: 3
--- !u!114 &1149029900162062154 --- !u!114 &1149029900162062154
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
...@@ -298,7 +298,7 @@ MonoBehaviour: ...@@ -298,7 +298,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 2de495c310a322143be08aa7819a1a33, type: 3} m_Script: {fileID: 11500000, guid: 2de495c310a322143be08aa7819a1a33, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
DamageOnTrigger: 0 DamageOnTrigger: 20
--- !u!1001 &7309850180045348080 --- !u!1001 &7309850180045348080
PrefabInstance: PrefabInstance:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
......
...@@ -103,7 +103,7 @@ MonoBehaviour: ...@@ -103,7 +103,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 2de495c310a322143be08aa7819a1a33, type: 3} m_Script: {fileID: 11500000, guid: 2de495c310a322143be08aa7819a1a33, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
DamageOnTrigger: 50 DamageOnTrigger: 10
--- !u!114 &-8527907258641966621 --- !u!114 &-8527907258641966621
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
...@@ -129,7 +129,7 @@ MonoBehaviour: ...@@ -129,7 +129,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d8b4a5cfe7236db4dba833c5751c2665, type: 3} m_Script: {fileID: 11500000, guid: d8b4a5cfe7236db4dba833c5751c2665, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
NpcTargetRadius: 80 NpcTargetRadius: 10
--- !u!114 &-274669407858188047 --- !u!114 &-274669407858188047
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
......
...@@ -100,7 +100,7 @@ MonoBehaviour: ...@@ -100,7 +100,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8194ce63a01ea9044a8c5a6ebd0e2829, type: 3} m_Script: {fileID: 11500000, guid: 8194ce63a01ea9044a8c5a6ebd0e2829, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
NpcTargetRadius: 80 NpcTargetRadius: 13
AttackCooldownTime: 2 AttackCooldownTime: 2
FirePointOffset: {x: 0, y: 1.5, z: 0} FirePointOffset: {x: 0, y: 1.5, z: 0}
AttackPrefab: {fileID: 357967689053387235, guid: 19127a4c1ac11844db4373b9e147918b, type: 3} AttackPrefab: {fileID: 357967689053387235, guid: 19127a4c1ac11844db4373b9e147918b, type: 3}
......
...@@ -149,8 +149,8 @@ MonoBehaviour: ...@@ -149,8 +149,8 @@ MonoBehaviour:
m_EditorClassIdentifier: m_EditorClassIdentifier:
sprintRemaining: 5 sprintRemaining: 5
sprintDuration: 5 sprintDuration: 5
sprintSpeed: 12 sprintSpeed: 7
walkSpeed: 8 walkSpeed: 5
sprintCooldownReset: 1 sprintCooldownReset: 1
--- !u!114 &3955402020692204287 --- !u!114 &3955402020692204287
MonoBehaviour: MonoBehaviour:
...@@ -320,7 +320,7 @@ MonoBehaviour: ...@@ -320,7 +320,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8e73c985659b81d408f4e99cb152349d, type: 3} m_Script: {fileID: 11500000, guid: 8e73c985659b81d408f4e99cb152349d, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
MaxHitPoints: 1000 MaxHitPoints: 200
HealthBarOffset: {x: 0, y: 2.5, z: 0} HealthBarOffset: {x: 0, y: 2.5, z: 0}
--- !u!1001 &6975352639711469968 --- !u!1001 &6975352639711469968
PrefabInstance: PrefabInstance:
......
...@@ -63,7 +63,7 @@ MonoBehaviour: ...@@ -63,7 +63,7 @@ MonoBehaviour:
OptimizationMode: 0 OptimizationMode: 0
Importance: 10 Importance: 10
MaxSendRate: 0 MaxSendRate: 0
prefabId: prefabId: b3cdfc2eac1a20f4fa01943b331adf7e
HasOwner: 0 HasOwner: 0
SupportAutoCommandTarget: 1 SupportAutoCommandTarget: 1
TrackInterpolationDelay: 0 TrackInterpolationDelay: 0
...@@ -84,4 +84,4 @@ MonoBehaviour: ...@@ -84,4 +84,4 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
RespawnTime: 10 RespawnTime: 10
NetCodeConfig: {fileID: 11400000, guid: cd69de227738309429193c9089949c16, type: 2} NetCodeConfig: {fileID: 11400000, guid: edda5bd0f33a95e4a98f780e0e3ae9e8, type: 2}
...@@ -489,9 +489,9 @@ MonoBehaviour: ...@@ -489,9 +489,9 @@ MonoBehaviour:
RangerAmountContainer: {fileID: 7438304585132253662} RangerAmountContainer: {fileID: 7438304585132253662}
SlimeAmountContainer: {fileID: 7438304585132253661} SlimeAmountContainer: {fileID: 7438304585132253661}
_connectButton: {fileID: 661734915} _connectButton: {fileID: 661734915}
_gameStartCountDownTime: 2 _gameStartCountDownTime: 10
_slimeSpawnCooldownTime: 0.01 _slimeSpawnCooldownTime: 1
_rogueSpawnCooldownTime: 0.1 _rogueSpawnCooldownTime: 2
--- !u!114 &1734898360 stripped --- !u!114 &1734898360 stripped
MonoBehaviour: MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 8650075725555599494, guid: 72327351aa28fbe4fb4c16c3d17e4de5, type: 3} m_CorrespondingSourceObject: {fileID: 8650075725555599494, guid: 72327351aa28fbe4fb4c16c3d17e4de5, type: 3}
......
...@@ -336,6 +336,26 @@ PrefabInstance: ...@@ -336,6 +336,26 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z propertyPath: m_LocalEulerAnglesHint.z
value: 0 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4973084550295746815, guid: dc695439125c44a4190bdc008aa5a0fc, type: 3}
propertyPath: m_IsActive
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5689824232254750683, guid: dc695439125c44a4190bdc008aa5a0fc, type: 3}
propertyPath: m_text
value: 10
objectReference: {fileID: 0}
- target: {fileID: 5823419856895464815, guid: dc695439125c44a4190bdc008aa5a0fc, type: 3}
propertyPath: m_text
value: 10
objectReference: {fileID: 0}
- target: {fileID: 5823419856895464815, guid: dc695439125c44a4190bdc008aa5a0fc, type: 3}
propertyPath: 'm_ActiveFontFeatures.Array.data[0]'
value: 1801810542
objectReference: {fileID: 0}
- target: {fileID: 8914969948828194284, guid: dc695439125c44a4190bdc008aa5a0fc, type: 3}
propertyPath: m_IsActive
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: [] m_RemovedComponents: []
m_RemovedGameObjects: [] m_RemovedGameObjects: []
m_AddedGameObjects: [] m_AddedGameObjects: []
......
...@@ -9,11 +9,11 @@ EditorBuildSettings: ...@@ -9,11 +9,11 @@ EditorBuildSettings:
path: Assets/_Game/Scenes/ConnectionScene.unity path: Assets/_Game/Scenes/ConnectionScene.unity
guid: 5652506fc72e4fa42a51d693df592812 guid: 5652506fc72e4fa42a51d693df592812
- enabled: 1 - enabled: 1
path: Assets/_Game/Scenes/TestGameScene.unity
guid: a3178e3c90053df499a40c2abd214157
- enabled: 1
path: Assets/_Game/Scenes/GameScene.unity path: Assets/_Game/Scenes/GameScene.unity
guid: 99c9720ab356a0642a771bea13969a05 guid: 99c9720ab356a0642a771bea13969a05
- enabled: 1
path: Assets/_Game/Scenes/TestGameScene.unity
guid: a3178e3c90053df499a40c2abd214157
m_configObjects: m_configObjects:
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 14b037eabac02c946b6a54f5e7289a64, type: 3} com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 14b037eabac02c946b6a54f5e7289a64, type: 3}
m_UseUCBPForAssetBundles: 0 m_UseUCBPForAssetBundles: 0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment