Add attribute system with core stats and gameplay components
This commit is contained in:
2
GameCore/.gitignore
vendored
Normal file
2
GameCore/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
bin/
|
||||
.idea/
|
||||
31
GameCore/Attributes/Attribute.cs
Normal file
31
GameCore/Attributes/Attribute.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace GameCore.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Defines all possible stats an entity can have.
|
||||
/// </summary>
|
||||
public enum Attribute
|
||||
{
|
||||
// Core Stats
|
||||
Health,
|
||||
MaxHealth,
|
||||
Armor,
|
||||
MoveSpeed,
|
||||
Acceleration,
|
||||
Friction,
|
||||
JumpHeight,
|
||||
|
||||
// Combat Stats
|
||||
Damage,
|
||||
AttackSpeed,
|
||||
AttackRange,
|
||||
MeleeDamage, // Multiplier
|
||||
RangedDamage, // Multiplier
|
||||
|
||||
// Progression
|
||||
Level,
|
||||
Experience,
|
||||
ExperienceToNextLevel,
|
||||
|
||||
// Misc
|
||||
Luck
|
||||
}
|
||||
42
GameCore/Attributes/AttributeComponent.cs
Normal file
42
GameCore/Attributes/AttributeComponent.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// A component that stores all gameplay-relevant stats for an entity.
|
||||
/// This design is adapted directly from the excellent CharacterAttributes class
|
||||
/// in the 'broberry' project. It's a flexible, data-driven way to manage stats.
|
||||
/// </summary>
|
||||
public class AttributeComponent : IComponent
|
||||
{
|
||||
public readonly Dictionary<Attribute, float> BaseValues = new();
|
||||
public readonly Dictionary<Attribute, float> CurrentValues = new();
|
||||
|
||||
public event Action<Attribute, float>? OnAttributeChanged;
|
||||
|
||||
public void SetBaseValue(Attribute attr, float value)
|
||||
{
|
||||
BaseValues[attr] = value;
|
||||
SetCurrentValue(attr, value);
|
||||
}
|
||||
|
||||
public float GetValue(Attribute attr)
|
||||
{
|
||||
return CurrentValues.GetValueOrDefault(attr, 0f);
|
||||
}
|
||||
|
||||
public void ModifyValue(Attribute attr, float delta)
|
||||
{
|
||||
var newValue = GetValue(attr) + delta;
|
||||
SetCurrentValue(attr, newValue);
|
||||
}
|
||||
|
||||
public void SetCurrentValue(Attribute attr, float value, bool force = false)
|
||||
{
|
||||
if (CurrentValues.TryGetValue(attr, out var oldValue) && !(System.Math.Abs(oldValue - value) > 0.001f) &&
|
||||
!force) return;
|
||||
|
||||
CurrentValues[attr] = value;
|
||||
OnAttributeChanged?.Invoke(attr, value);
|
||||
}
|
||||
}
|
||||
23
GameCore/Attributes/AttributeSystem.cs
Normal file
23
GameCore/Attributes/AttributeSystem.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Attributes;
|
||||
|
||||
public class AttributeSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var entities = world.GetEntitiesWith<AttributeComponent>();
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var attributes = world.GetComponent<AttributeComponent>(entity);
|
||||
if (attributes == null) continue;
|
||||
|
||||
var maxHealth = attributes.GetValue(Attribute.MaxHealth);
|
||||
var currentHealth = attributes.GetValue(Attribute.Health);
|
||||
|
||||
if (currentHealth > maxHealth) attributes.SetCurrentValue(Attribute.Health, maxHealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
GameCore/Combat/AttackType.cs
Normal file
8
GameCore/Combat/AttackType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public enum AttackType
|
||||
{
|
||||
Hitscan = 0,
|
||||
Projectile = 1,
|
||||
Melee = 2
|
||||
}
|
||||
15
GameCore/Combat/DamageEventComponent.cs
Normal file
15
GameCore/Combat/DamageEventComponent.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
/// <summary>
|
||||
/// A temporary, event-like component. When this component is added to an entity,
|
||||
/// the DamageSystem will process it, apply damage to the target, and then
|
||||
/// immediately remove this component. This simulates a one-time damage event.
|
||||
/// </summary>
|
||||
public class DamageEventComponent(Entity target, float amount) : IComponent
|
||||
{
|
||||
public Entity Target { get; set; } = target;
|
||||
public float Amount { get; set; } = amount;
|
||||
}
|
||||
38
GameCore/Combat/DamageSystem.cs
Normal file
38
GameCore/Combat/DamageSystem.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using GameCore.Attributes;
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Events;
|
||||
using Attribute = GameCore.Attributes.Attribute;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class DamageSystem : ISystem
|
||||
{
|
||||
private readonly World _world;
|
||||
|
||||
public DamageSystem(World world)
|
||||
{
|
||||
_world = world;
|
||||
world.Subscribe<DamageDealtEvent>(OnDamageDealt);
|
||||
}
|
||||
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var entitiesWithHealth = world.GetEntitiesWith<AttributeComponent>();
|
||||
foreach (var entity in entitiesWithHealth)
|
||||
{
|
||||
if (world.GetComponent<DeathComponent>(entity) != null) continue;
|
||||
|
||||
var attributes = world.GetComponent<AttributeComponent>(entity);
|
||||
if (attributes == null) continue;
|
||||
|
||||
if (attributes.GetValue(Attribute.Health) <= 0) world.AddComponent(entity, new DeathComponent());
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDamageDealt(DamageDealtEvent e)
|
||||
{
|
||||
var targetAttributes = _world.GetComponent<AttributeComponent>(e.Target);
|
||||
targetAttributes?.ModifyValue(Attribute.Health, -e.Amount);
|
||||
}
|
||||
}
|
||||
13
GameCore/Combat/DeathComponent.cs
Normal file
13
GameCore/Combat/DeathComponent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
/// <summary>
|
||||
/// A simple "marker" component. When the DamageSystem determines an entity's
|
||||
/// health has dropped to 0 or below, it adds this component to the entity.
|
||||
/// Other systems (or the presenter layer) can then react to this, for example,
|
||||
/// by playing a death animation, creating a ragdoll, and eventually destroying the entity.
|
||||
/// </summary>
|
||||
public class DeathComponent : IComponent
|
||||
{
|
||||
}
|
||||
19
GameCore/Combat/DestructionSystem.cs
Normal file
19
GameCore/Combat/DestructionSystem.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Events;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class DestructionSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var entitiesWithDeath = world.GetEntitiesWith<DeathComponent>();
|
||||
foreach (var entity in entitiesWithDeath)
|
||||
{
|
||||
world.PublishEvent(new EntityDiedEvent(entity));
|
||||
|
||||
world.DestroyEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
GameCore/Combat/EffectExecutionSystem.cs
Normal file
12
GameCore/Combat/EffectExecutionSystem.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class EffectExecutionSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
48
GameCore/Combat/Effects/BulkProjectileEffect.cs
Normal file
48
GameCore/Combat/Effects/BulkProjectileEffect.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using GameCore.Combat.Interfaces;
|
||||
using GameCore.Events;
|
||||
using GameCore.Input;
|
||||
using GameCore.Movement;
|
||||
|
||||
namespace GameCore.Combat.Effects;
|
||||
|
||||
public class BulkProjectileEffect(string archetypeId, int count, float spreadAngle, float speed) : IEffect
|
||||
{
|
||||
private static readonly Random _random = new();
|
||||
|
||||
public void Execute(EffectContext context)
|
||||
{
|
||||
var ownerInput = context.World.GetComponent<InputStateComponent>(context.Owner);
|
||||
if (ownerInput == null)
|
||||
return;
|
||||
|
||||
var ownerRotation = context.World.GetComponent<RotationComponent>(context.Owner);
|
||||
if (ownerRotation == null) return;
|
||||
|
||||
var spreadRadians = spreadAngle * (float)System.Math.PI / 180f;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var direction = ownerInput.MuzzleDirection;
|
||||
|
||||
if (spreadRadians > 0f)
|
||||
{
|
||||
var randomYaw = ((float)_random.NextDouble() * 2f - 1f) * spreadRadians;
|
||||
var randomPitch = ((float)_random.NextDouble() * 2f - 1f) * spreadRadians;
|
||||
|
||||
direction = context.World.WorldQuery.RotateVectorByYaw(direction, randomYaw);
|
||||
direction.Y += (float)System.Math.Sin(randomPitch);
|
||||
direction = direction.Normalize();
|
||||
}
|
||||
|
||||
var initialVelocity = direction * speed;
|
||||
|
||||
context.World.PublishEvent(new SpawnEntityEvent(
|
||||
archetypeId,
|
||||
ownerInput.MuzzlePosition,
|
||||
ownerRotation.Rotation,
|
||||
context.Owner,
|
||||
initialVelocity
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
15
GameCore/Combat/Effects/DamageEffect.cs
Normal file
15
GameCore/Combat/Effects/DamageEffect.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using GameCore.Combat.Interfaces;
|
||||
using GameCore.Events;
|
||||
|
||||
namespace GameCore.Combat.Effects;
|
||||
|
||||
public class DamageEffect(float amount) : IEffect
|
||||
{
|
||||
public void Execute(EffectContext context)
|
||||
{
|
||||
if (context.Target == null)
|
||||
return;
|
||||
|
||||
context.World.PublishEvent(new DamageDealtEvent(context.Target.Value, amount));
|
||||
}
|
||||
}
|
||||
10
GameCore/Combat/Effects/EffectContext.cs
Normal file
10
GameCore/Combat/Effects/EffectContext.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using GameCore.ECS;
|
||||
|
||||
namespace GameCore.Combat.Effects;
|
||||
|
||||
public class EffectContext
|
||||
{
|
||||
public Entity Owner;
|
||||
public Entity? Target;
|
||||
public World World;
|
||||
}
|
||||
30
GameCore/Combat/Effects/HitscanEffect.cs
Normal file
30
GameCore/Combat/Effects/HitscanEffect.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using GameCore.Combat.Interfaces;
|
||||
using GameCore.Input;
|
||||
|
||||
namespace GameCore.Combat.Effects;
|
||||
|
||||
public class HitscanEffect(float range) : IEffect
|
||||
{
|
||||
public void Execute(EffectContext context)
|
||||
{
|
||||
var input = context.World.GetComponent<InputStateComponent>(context.Owner);
|
||||
var weapon = context.World.GetComponent<WeaponComponent>(context.Owner);
|
||||
|
||||
if (input == null || weapon == null) return;
|
||||
|
||||
var targetPos = input.MuzzlePosition + input.MuzzleDirection * range;
|
||||
var hit = context.World.WorldQuery.Raycast(input.MuzzlePosition, targetPos, context.Owner);
|
||||
|
||||
if (hit.DidHit)
|
||||
{
|
||||
var hitContext = new EffectContext
|
||||
{
|
||||
World = context.World,
|
||||
Owner = context.Owner,
|
||||
Target = hit.HitEntity
|
||||
};
|
||||
|
||||
foreach (var effect in weapon.OnHitEffects) effect.Execute(hitContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
GameCore/Combat/Interfaces/IEffect.cs
Normal file
8
GameCore/Combat/Interfaces/IEffect.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using GameCore.Combat.Effects;
|
||||
|
||||
namespace GameCore.Combat.Interfaces;
|
||||
|
||||
public interface IEffect
|
||||
{
|
||||
void Execute(EffectContext context);
|
||||
}
|
||||
25
GameCore/Combat/ProjectileCleanupSystem.cs
Normal file
25
GameCore/Combat/ProjectileCleanupSystem.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Physics;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class ProjectileCleanupSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var projectiles = world.GetEntitiesWith<ProjectileComponent>();
|
||||
foreach (var projectile in projectiles)
|
||||
{
|
||||
var projectileData = world.GetComponent<ProjectileComponent>(projectile);
|
||||
var position = world.GetComponent<PositionComponent>(projectile);
|
||||
var velocity = world.GetComponent<VelocityComponent>(projectile);
|
||||
|
||||
if (projectileData == null || position == null || velocity == null)
|
||||
continue;
|
||||
|
||||
projectileData.Lifetime -= deltaTime;
|
||||
if (projectileData.Lifetime <= 0f) world.AddComponent(projectile, new DeathComponent());
|
||||
}
|
||||
}
|
||||
}
|
||||
13
GameCore/Combat/ProjectileComponent.cs
Normal file
13
GameCore/Combat/ProjectileComponent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using GameCore.Combat.Interfaces;
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class ProjectileComponent : IComponent
|
||||
{
|
||||
public Entity Owner { get; set; }
|
||||
public float Lifetime { get; set; }
|
||||
|
||||
public List<IEffect> OnHitEffects { get; set; } = [];
|
||||
}
|
||||
33
GameCore/Combat/ProjectileInitializationSystem.cs
Normal file
33
GameCore/Combat/ProjectileInitializationSystem.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Events;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class ProjectileInitializationSystem : ISystem
|
||||
{
|
||||
private readonly World _world;
|
||||
|
||||
public ProjectileInitializationSystem(World world)
|
||||
{
|
||||
_world = world;
|
||||
_world.Subscribe<EntitySpawnedEvent>(OnEntitySpawned);
|
||||
}
|
||||
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
}
|
||||
|
||||
private void OnEntitySpawned(EntitySpawnedEvent e)
|
||||
{
|
||||
var projComp = _world.GetComponent<ProjectileComponent>(e.NewEntity);
|
||||
if (projComp == null) return;
|
||||
|
||||
var ownerWeapon = _world.GetComponent<WeaponComponent>(e.Owner);
|
||||
if (ownerWeapon != null)
|
||||
{
|
||||
projComp.Owner = e.Owner;
|
||||
projComp.OnHitEffects = ownerWeapon.OnHitEffects;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
GameCore/Combat/ProjectileSystem.cs
Normal file
43
GameCore/Combat/ProjectileSystem.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using GameCore.Combat.Effects;
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Physics;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class ProjectileSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var projectiles = world.GetEntitiesWith<ProjectileComponent>();
|
||||
foreach (var projectile in projectiles)
|
||||
{
|
||||
var velocity = world.GetComponent<VelocityComponent>(projectile);
|
||||
var position = world.GetComponent<PositionComponent>(projectile);
|
||||
var projectileData = world.GetComponent<ProjectileComponent>(projectile);
|
||||
|
||||
if (velocity == null || position == null || projectileData == null)
|
||||
continue;
|
||||
|
||||
var newPosition = position.Position + velocity.DesiredVelocity * deltaTime;
|
||||
|
||||
var hit = world.WorldQuery.Raycast(position.Position, newPosition, projectileData.Owner);
|
||||
if (hit.DidHit)
|
||||
{
|
||||
var hitContext = new EffectContext
|
||||
{
|
||||
World = world,
|
||||
Owner = projectileData.Owner,
|
||||
Target = hit.HitEntity
|
||||
};
|
||||
|
||||
foreach (var effect in projectileData.OnHitEffects) effect.Execute(hitContext);
|
||||
|
||||
world.AddComponent(projectile, new DeathComponent());
|
||||
continue;
|
||||
}
|
||||
|
||||
position.Position = newPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
GameCore/Combat/WeaponComponent.cs
Normal file
14
GameCore/Combat/WeaponComponent.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using GameCore.Combat.Interfaces;
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class WeaponComponent : IComponent
|
||||
{
|
||||
public float FireRate { get; set; } = 1f;
|
||||
|
||||
public List<IEffect> OnFireEffects { get; set; } = [];
|
||||
public List<IEffect> OnHitEffects { get; set; } = [];
|
||||
|
||||
public float CooldownTimer { get; set; } = 0f;
|
||||
}
|
||||
39
GameCore/Combat/WeaponSystem.cs
Normal file
39
GameCore/Combat/WeaponSystem.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using GameCore.Combat.Effects;
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Events;
|
||||
using GameCore.Input;
|
||||
using GameCore.Movement;
|
||||
using GameCore.Physics;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class WeaponSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var entities = world.GetEntitiesWith<InputStateComponent>();
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var input = world.GetComponent<InputStateComponent>(entity);
|
||||
var weapon = world.GetComponent<WeaponComponent>(entity);
|
||||
var position = world.GetComponent<PositionComponent>(entity);
|
||||
var rotation = world.GetComponent<RotationComponent>(entity);
|
||||
|
||||
if (input == null || weapon == null || position == null || rotation == null)
|
||||
continue;
|
||||
|
||||
if (weapon.CooldownTimer > 0f) weapon.CooldownTimer -= deltaTime;
|
||||
|
||||
if (input.IsFiring && weapon.CooldownTimer <= 0f)
|
||||
{
|
||||
var context = new EffectContext { World = world, Owner = entity };
|
||||
|
||||
foreach (var effect in weapon.OnFireEffects) effect.Execute(context);
|
||||
|
||||
world.PublishEvent(new WeaponFiredEvent(entity, input.MuzzlePosition));
|
||||
weapon.CooldownTimer = 1f / weapon.FireRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
GameCore/Config/SimulationConfig.cs
Normal file
6
GameCore/Config/SimulationConfig.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace GameCore.Config;
|
||||
|
||||
public class SimulationConfig
|
||||
{
|
||||
public float GravityStrength { get; set; } = 9.81f;
|
||||
}
|
||||
11
GameCore/ECS/Entity.cs
Normal file
11
GameCore/ECS/Entity.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace GameCore.ECS;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a unique object in the game world.
|
||||
/// It's a simple struct containing an ID to keep it lightweight.
|
||||
/// All data associated with an Entity is stored in Components.
|
||||
/// </summary>
|
||||
public readonly struct Entity(int id)
|
||||
{
|
||||
public readonly int Id = id;
|
||||
}
|
||||
7
GameCore/ECS/HitResult.cs
Normal file
7
GameCore/ECS/HitResult.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace GameCore.ECS;
|
||||
|
||||
public struct HitResult
|
||||
{
|
||||
public bool DidHit;
|
||||
public Entity HitEntity;
|
||||
}
|
||||
12
GameCore/ECS/Interfaces/IComponent.cs
Normal file
12
GameCore/ECS/Interfaces/IComponent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace GameCore.ECS.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// A marker interface for all components.
|
||||
/// Components are simple data containers (structs or classes) that hold the state
|
||||
/// for a specific aspect of an Entity (e.g., its position, health, or inventory).
|
||||
/// They contain no logic.
|
||||
/// </summary>
|
||||
public interface IComponent
|
||||
{
|
||||
|
||||
}
|
||||
6
GameCore/ECS/Interfaces/IEntityPresenter.cs
Normal file
6
GameCore/ECS/Interfaces/IEntityPresenter.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace GameCore.ECS.Interfaces;
|
||||
|
||||
public interface IEntityPresenter
|
||||
{
|
||||
public Entity CoreEntity { get; set; }
|
||||
}
|
||||
8
GameCore/ECS/Interfaces/IPresenterComponent.cs
Normal file
8
GameCore/ECS/Interfaces/IPresenterComponent.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace GameCore.ECS.Interfaces;
|
||||
|
||||
public interface IPresenterComponent
|
||||
{
|
||||
void Initialize(Entity coreEntity, World world);
|
||||
void SyncToPresentation(float delta);
|
||||
void SyncToCore(float delta);
|
||||
}
|
||||
17
GameCore/ECS/Interfaces/ISystem.cs
Normal file
17
GameCore/ECS/Interfaces/ISystem.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace GameCore.ECS.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// The contract for all game logic systems.
|
||||
/// Systems are stateless classes that contain all the logic.
|
||||
/// They operate on entities that possess a specific set of components.
|
||||
/// For example, a MovementSystem would operate on entities with PositionComponent and VelocityComponent.
|
||||
/// </summary>
|
||||
public interface ISystem
|
||||
{
|
||||
/// <summary>
|
||||
/// The main update method for a system, called once per game loop.
|
||||
/// </summary>
|
||||
/// <param name="world">A reference to the main World object to query for entities and components.</param>
|
||||
/// <param name="deltaTime">The time elapsed since the last frame.</param>
|
||||
void Update(World world, float deltaTime);
|
||||
}
|
||||
9
GameCore/ECS/Interfaces/IWorldQuery.cs
Normal file
9
GameCore/ECS/Interfaces/IWorldQuery.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using GameCore.Math;
|
||||
|
||||
namespace GameCore.ECS.Interfaces;
|
||||
|
||||
public interface IWorldQuery
|
||||
{
|
||||
HitResult Raycast(Vector3 from, Vector3 to, Entity? ownerToExclude = null);
|
||||
Vector3 RotateVectorByYaw(Vector3 vector, float yawRadians);
|
||||
}
|
||||
10
GameCore/ECS/PresenterData.cs
Normal file
10
GameCore/ECS/PresenterData.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.ECS;
|
||||
|
||||
public readonly struct PresenterData(Entity entity, IEntityPresenter presenter, List<IPresenterComponent> components)
|
||||
{
|
||||
public readonly Entity Entity = entity;
|
||||
public readonly IEntityPresenter Presenter = presenter;
|
||||
public readonly List<IPresenterComponent> Components = components;
|
||||
}
|
||||
113
GameCore/ECS/World.cs
Normal file
113
GameCore/ECS/World.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using GameCore.Config;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Events;
|
||||
using GameCore.Events.Interfaces;
|
||||
using GameCore.Input.Interfaces;
|
||||
|
||||
namespace GameCore.ECS;
|
||||
|
||||
/// <summary>
|
||||
/// The central container for the entire game state.
|
||||
/// It manages all entities, components, and systems, and orchestrates the main game loop.
|
||||
/// This class is the primary point of interaction for the Engine/Presentation layer.
|
||||
/// </summary>
|
||||
public class World(IInputService inputService, IWorldQuery worldQuery, SimulationConfig config)
|
||||
{
|
||||
private readonly Dictionary<int, List<IComponent>> _componentsByEntityId = new();
|
||||
private readonly EventBus _eventBus = new();
|
||||
private readonly List<ISystem> _systems = [];
|
||||
|
||||
public readonly SimulationConfig Config = config;
|
||||
public readonly IInputService InputService = inputService;
|
||||
public readonly IWorldQuery WorldQuery = worldQuery;
|
||||
private int _nextEntityId;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new, unique entity.
|
||||
/// </summary>
|
||||
/// <returns>The newly created Entity.</returns>
|
||||
public Entity CreateEntity()
|
||||
{
|
||||
var id = _nextEntityId++;
|
||||
_componentsByEntityId[id] = [];
|
||||
return new Entity(id);
|
||||
}
|
||||
|
||||
public void DestroyEntity(Entity entity)
|
||||
{
|
||||
_componentsByEntityId.Remove(entity.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a component to a given entity.
|
||||
/// </summary>
|
||||
public void AddComponent(Entity entity, IComponent component)
|
||||
{
|
||||
_componentsByEntityId[entity.Id].Add(component);
|
||||
}
|
||||
|
||||
public void RemoveComponent<T>(Entity entity) where T : class, IComponent
|
||||
{
|
||||
if (!_componentsByEntityId.TryGetValue(entity.Id, out var components)) return;
|
||||
|
||||
var componentToRemove = components.OfType<T>().FirstOrDefault();
|
||||
if (componentToRemove != null) components.Remove(componentToRemove);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a specific type of component from an entity.
|
||||
/// </summary>
|
||||
/// <returns>The component instance, or null if the entity doesn't have it.</returns>
|
||||
public T? GetComponent<T>(Entity entity) where T : class, IComponent
|
||||
{
|
||||
return _componentsByEntityId.TryGetValue(entity.Id, out var components)
|
||||
? components.OfType<T>().FirstOrDefault()
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all entities that have a specific component type.
|
||||
/// </summary>
|
||||
public IEnumerable<Entity> GetEntitiesWith<T>() where T : IComponent
|
||||
{
|
||||
return from pair in _componentsByEntityId
|
||||
where pair.Value.Any(c => c is T)
|
||||
select new Entity(pair.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a system to be run in the game loop.
|
||||
/// </summary>
|
||||
public void RegisterSystem(ISystem system)
|
||||
{
|
||||
_systems.Add(system);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a single tick of the game simulation, updating all registered systems.
|
||||
/// </summary>
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
foreach (var system in _systems) system.Update(this, deltaTime);
|
||||
}
|
||||
|
||||
public void PublishEvent(IEvent gameEvent)
|
||||
{
|
||||
_eventBus.Publish(gameEvent);
|
||||
}
|
||||
|
||||
public void ProcessEvents()
|
||||
{
|
||||
_eventBus.ProcessEvents();
|
||||
}
|
||||
|
||||
public void Subscribe<T>(Action<T> handler) where T : IEvent
|
||||
{
|
||||
_eventBus.Subscribe(handler);
|
||||
}
|
||||
|
||||
public void Unsubscribe<T>(Action<T> handler) where T : IEvent
|
||||
{
|
||||
_eventBus.Unsubscribe(handler);
|
||||
}
|
||||
}
|
||||
10
GameCore/Events/DamageDealtEvent.cs
Normal file
10
GameCore/Events/DamageDealtEvent.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.Events.Interfaces;
|
||||
|
||||
namespace GameCore.Events;
|
||||
|
||||
public readonly struct DamageDealtEvent(Entity target, float amount) : IEvent
|
||||
{
|
||||
public readonly Entity Target = target;
|
||||
public readonly float Amount = amount;
|
||||
}
|
||||
9
GameCore/Events/EntityDiedEvent.cs
Normal file
9
GameCore/Events/EntityDiedEvent.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.Events.Interfaces;
|
||||
|
||||
namespace GameCore.Events;
|
||||
|
||||
public readonly struct EntityDiedEvent(Entity entity) : IEvent
|
||||
{
|
||||
public readonly Entity Entity = entity;
|
||||
}
|
||||
11
GameCore/Events/EntitySpawnedEvent.cs
Normal file
11
GameCore/Events/EntitySpawnedEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.Events.Interfaces;
|
||||
|
||||
namespace GameCore.Events;
|
||||
|
||||
public readonly struct EntitySpawnedEvent(Entity newEntity, Entity owner, string archetypeId) : IEvent
|
||||
{
|
||||
public readonly Entity NewEntity = newEntity;
|
||||
public readonly Entity Owner = owner;
|
||||
public readonly string ArchetypeId = archetypeId;
|
||||
}
|
||||
46
GameCore/Events/EventBus.cs
Normal file
46
GameCore/Events/EventBus.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using GameCore.Events.Interfaces;
|
||||
|
||||
namespace GameCore.Events;
|
||||
|
||||
public class EventBus
|
||||
{
|
||||
private readonly Queue<IEvent> _eventQueue = new();
|
||||
private readonly Dictionary<Type, List<Action<IEvent>>> _subscribers = new();
|
||||
|
||||
public void Subscribe<T>(Action<T> listener) where T : IEvent
|
||||
{
|
||||
var eventType = typeof(T);
|
||||
if (!_subscribers.ContainsKey(eventType)) _subscribers[eventType] = [];
|
||||
|
||||
_subscribers[eventType].Add(e => listener((T)e));
|
||||
}
|
||||
|
||||
public void Unsubscribe<T>(Action<T> listener) where T : IEvent
|
||||
{
|
||||
var eventType = typeof(T);
|
||||
if (!_subscribers.TryGetValue(eventType, out var listeners)) return;
|
||||
|
||||
listeners.RemoveAll(e => e.Equals((Action<IEvent>)(e1 => listener((T)e1))));
|
||||
|
||||
if (listeners.Count == 0) _subscribers.Remove(eventType);
|
||||
}
|
||||
|
||||
public void Publish(IEvent gameEvent)
|
||||
{
|
||||
_eventQueue.Enqueue(gameEvent);
|
||||
}
|
||||
|
||||
public void ProcessEvents()
|
||||
{
|
||||
while (_eventQueue.Count > 0)
|
||||
{
|
||||
var gameEvent = _eventQueue.Dequeue();
|
||||
var eventType = gameEvent.GetType();
|
||||
|
||||
if (!_subscribers.TryGetValue(eventType, out var listeners)) continue;
|
||||
|
||||
foreach (var listener in listeners)
|
||||
listener.Invoke(gameEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
GameCore/Events/Interfaces/IEvent.cs
Normal file
6
GameCore/Events/Interfaces/IEvent.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace GameCore.Events.Interfaces;
|
||||
|
||||
public interface IEvent
|
||||
{
|
||||
|
||||
}
|
||||
20
GameCore/Events/SpawnEntityEvent.cs
Normal file
20
GameCore/Events/SpawnEntityEvent.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.Events.Interfaces;
|
||||
using GameCore.Math;
|
||||
|
||||
namespace GameCore.Events;
|
||||
|
||||
public readonly struct SpawnEntityEvent(
|
||||
string archetypeId,
|
||||
Vector3 position,
|
||||
Vector3 rotation,
|
||||
Entity owner,
|
||||
Vector3? initialVelocity = null)
|
||||
: IEvent
|
||||
{
|
||||
public readonly string ArchetypeId = archetypeId;
|
||||
public readonly Vector3 Position = position;
|
||||
public readonly Vector3 Rotation = rotation;
|
||||
public readonly Entity Owner = owner;
|
||||
public readonly Vector3? InitialVelocity = initialVelocity;
|
||||
}
|
||||
11
GameCore/Events/WeaponFiredEvent.cs
Normal file
11
GameCore/Events/WeaponFiredEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.Events.Interfaces;
|
||||
using GameCore.Math;
|
||||
|
||||
namespace GameCore.Events;
|
||||
|
||||
public readonly struct WeaponFiredEvent(Entity owner, Vector3 muzzlePosition) : IEvent
|
||||
{
|
||||
public readonly Entity Owner = owner;
|
||||
public readonly Vector3 MuzzlePosition = muzzlePosition;
|
||||
}
|
||||
13
GameCore/GameCore.csproj
Normal file
13
GameCore/GameCore.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Factories\"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
16
GameCore/Input/InputStateComponent.cs
Normal file
16
GameCore/Input/InputStateComponent.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Input.Interfaces;
|
||||
using GameCore.Math;
|
||||
|
||||
namespace GameCore.Input;
|
||||
|
||||
public class InputStateComponent : IComponent, IInputService
|
||||
{
|
||||
public Vector3 MuzzlePosition { get; set; }
|
||||
public Vector3 MuzzleDirection { get; set; }
|
||||
public bool IsFiring { get; set; }
|
||||
public bool IsInteracting { get; set; }
|
||||
public bool IsJumping { get; set; }
|
||||
public Vector3 MoveDirection { get; set; }
|
||||
public Vector3 LookDirection { get; set; }
|
||||
}
|
||||
12
GameCore/Input/Interfaces/IInputService.cs
Normal file
12
GameCore/Input/Interfaces/IInputService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using GameCore.Math;
|
||||
|
||||
namespace GameCore.Input.Interfaces;
|
||||
|
||||
public interface IInputService
|
||||
{
|
||||
public bool IsFiring { get; }
|
||||
public bool IsInteracting { get; }
|
||||
public bool IsJumping { get; }
|
||||
public Vector3 MoveDirection { get; }
|
||||
public Vector3 LookDirection { get; }
|
||||
}
|
||||
24
GameCore/Input/PlayerInputSystem.cs
Normal file
24
GameCore/Input/PlayerInputSystem.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Player;
|
||||
|
||||
namespace GameCore.Input;
|
||||
|
||||
public class PlayerInputSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var playerEntities = world.GetEntitiesWith<PlayerComponent>();
|
||||
if (!playerEntities.Any()) return;
|
||||
|
||||
var playerEntity = playerEntities.First();
|
||||
var inputState = world.GetComponent<InputStateComponent>(playerEntity);
|
||||
if (inputState == null) return;
|
||||
|
||||
inputState.MoveDirection = world.InputService.MoveDirection.Normalize();
|
||||
inputState.LookDirection = world.InputService.LookDirection;
|
||||
inputState.IsJumping = world.InputService.IsJumping;
|
||||
inputState.IsInteracting = world.InputService.IsInteracting;
|
||||
inputState.IsFiring = world.InputService.IsFiring;
|
||||
}
|
||||
}
|
||||
79
GameCore/Math/Vector3.cs
Normal file
79
GameCore/Math/Vector3.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
namespace GameCore.Math;
|
||||
|
||||
/// <summary>
|
||||
/// A simple, engine-agnostic 3D vector struct.
|
||||
/// We use our own to avoid dependencies on Godot or Unity's vector types
|
||||
/// within the GameCore library, ensuring it remains portable.
|
||||
/// </summary>
|
||||
public struct Vector3
|
||||
{
|
||||
public float X;
|
||||
public float Y;
|
||||
public float Z;
|
||||
|
||||
public static readonly Vector3 Zero = new(0, 0, 0);
|
||||
|
||||
public Vector3(float x, float y, float z)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
}
|
||||
|
||||
public static Vector3 operator +(Vector3 a, Vector3 b)
|
||||
{
|
||||
return new Vector3(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
|
||||
}
|
||||
|
||||
public static Vector3 operator -(Vector3 a, Vector3 b)
|
||||
{
|
||||
return new Vector3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
|
||||
}
|
||||
|
||||
public static Vector3 operator *(Vector3 a, float d)
|
||||
{
|
||||
return new Vector3(a.X * d, a.Y * d, a.Z * d);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"({X}, {Y}, {Z})";
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is not Vector3 other)
|
||||
return false;
|
||||
|
||||
return X == other.X && Y == other.Y && Z == other.Z;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(X, Y, Z);
|
||||
}
|
||||
|
||||
public static bool operator ==(Vector3 a, Vector3 b)
|
||||
{
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(Vector3 a, Vector3 b)
|
||||
{
|
||||
return !a.Equals(b);
|
||||
}
|
||||
|
||||
public float Length()
|
||||
{
|
||||
return (float)System.Math.Sqrt(X * X + Y * Y + Z * Z);
|
||||
}
|
||||
|
||||
public Vector3 Normalize()
|
||||
{
|
||||
var length = Length();
|
||||
if (length == 0)
|
||||
return Zero;
|
||||
|
||||
return new Vector3(X / length, Y / length, Z / length);
|
||||
}
|
||||
}
|
||||
8
GameCore/Movement/CharacterStateComponent.cs
Normal file
8
GameCore/Movement/CharacterStateComponent.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Movement;
|
||||
|
||||
public class CharacterStateComponent : IComponent
|
||||
{
|
||||
public bool IsOnFloor { get; set; }
|
||||
}
|
||||
23
GameCore/Movement/GravitySystem.cs
Normal file
23
GameCore/Movement/GravitySystem.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Physics;
|
||||
|
||||
namespace GameCore.Movement;
|
||||
|
||||
public class GravitySystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var entities = world.GetEntitiesWith<VelocityComponent>();
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var velocity = world.GetComponent<VelocityComponent>(entity);
|
||||
var characterState = world.GetComponent<CharacterStateComponent>(entity);
|
||||
|
||||
if (velocity == null || characterState == null)
|
||||
continue;
|
||||
|
||||
if (!characterState.IsOnFloor) velocity.DesiredVelocity.Y -= world.Config.GravityStrength * deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
61
GameCore/Movement/GroundMovementSystem.cs
Normal file
61
GameCore/Movement/GroundMovementSystem.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using GameCore.Attributes;
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Input;
|
||||
using GameCore.Math;
|
||||
using GameCore.Physics;
|
||||
using Attribute = GameCore.Attributes.Attribute;
|
||||
|
||||
namespace GameCore.Movement;
|
||||
|
||||
public class GroundMovementSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var entities = world.GetEntitiesWith<InputStateComponent>();
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var input = world.GetComponent<InputStateComponent>(entity);
|
||||
var velocity = world.GetComponent<VelocityComponent>(entity);
|
||||
var attributes = world.GetComponent<AttributeComponent>(entity);
|
||||
var rotation = world.GetComponent<RotationComponent>(entity);
|
||||
|
||||
if (input == null || velocity == null || attributes == null || rotation == null)
|
||||
continue;
|
||||
|
||||
var moveSpeed = attributes.GetValue(Attribute.MoveSpeed);
|
||||
var acceleration = attributes.GetValue(Attribute.Acceleration);
|
||||
var friction = attributes.GetValue(Attribute.Friction);
|
||||
|
||||
var yaw = rotation.Rotation.Y;
|
||||
var rotatedDir = world.WorldQuery.RotateVectorByYaw(input.MoveDirection, yaw);
|
||||
|
||||
var targetVelocity = new Vector3(
|
||||
rotatedDir.X * moveSpeed,
|
||||
velocity.DesiredVelocity.Y,
|
||||
rotatedDir.Z * moveSpeed
|
||||
);
|
||||
|
||||
if (rotatedDir.Length() >= 0.1f)
|
||||
{
|
||||
velocity.DesiredVelocity =
|
||||
MoveToward(velocity.DesiredVelocity, targetVelocity, acceleration * deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
var frictionVec = new Vector3(velocity.DesiredVelocity.X, 0f, velocity.DesiredVelocity.Z);
|
||||
frictionVec = MoveToward(frictionVec, Vector3.Zero, friction * deltaTime);
|
||||
velocity.DesiredVelocity.X = frictionVec.X;
|
||||
velocity.DesiredVelocity.Z = frictionVec.Z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 MoveToward(Vector3 from, Vector3 to, float delta)
|
||||
{
|
||||
var diff = to - from;
|
||||
if (diff.Length() <= delta)
|
||||
return to;
|
||||
return from + diff.Normalize() * delta;
|
||||
}
|
||||
}
|
||||
32
GameCore/Movement/JumpSystem.cs
Normal file
32
GameCore/Movement/JumpSystem.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using GameCore.Attributes;
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Input;
|
||||
using GameCore.Physics;
|
||||
using Attribute = GameCore.Attributes.Attribute;
|
||||
|
||||
namespace GameCore.Movement;
|
||||
|
||||
public class JumpSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var entities = world.GetEntitiesWith<InputStateComponent>();
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var input = world.GetComponent<InputStateComponent>(entity);
|
||||
var velocity = world.GetComponent<VelocityComponent>(entity);
|
||||
var attributes = world.GetComponent<AttributeComponent>(entity);
|
||||
var characterState = world.GetComponent<CharacterStateComponent>(entity);
|
||||
|
||||
if (input == null || velocity == null || attributes == null || characterState == null)
|
||||
continue;
|
||||
|
||||
if (input.IsJumping && characterState.IsOnFloor)
|
||||
{
|
||||
var jumpHeight = attributes.GetValue(Attribute.JumpHeight);
|
||||
velocity.DesiredVelocity.Y = (float)System.Math.Sqrt(2f * world.Config.GravityStrength * jumpHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
GameCore/Movement/RotationComponent.cs
Normal file
9
GameCore/Movement/RotationComponent.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Math;
|
||||
|
||||
namespace GameCore.Movement;
|
||||
|
||||
public class RotationComponent : IComponent
|
||||
{
|
||||
public Vector3 Rotation;
|
||||
}
|
||||
29
GameCore/Movement/RotationSystem.cs
Normal file
29
GameCore/Movement/RotationSystem.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Input;
|
||||
|
||||
namespace GameCore.Movement;
|
||||
|
||||
public class RotationSystem : ISystem
|
||||
{
|
||||
private const float MinPitch = -(float)System.Math.PI / 2f;
|
||||
private const float MaxPitch = (float)System.Math.PI / 2f;
|
||||
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var entities = world.GetEntitiesWith<InputStateComponent>();
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var input = world.GetComponent<InputStateComponent>(entity);
|
||||
var rotation = world.GetComponent<RotationComponent>(entity);
|
||||
|
||||
if (input == null || rotation == null)
|
||||
continue;
|
||||
|
||||
rotation.Rotation.Y += input.LookDirection.Y;
|
||||
rotation.Rotation.X += input.LookDirection.X;
|
||||
|
||||
rotation.Rotation.X = System.Math.Clamp(rotation.Rotation.X, MinPitch, MaxPitch);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
GameCore/Physics/PositionComponent.cs
Normal file
12
GameCore/Physics/PositionComponent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Math;
|
||||
|
||||
namespace GameCore.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the 3D position of an entity in the game world.
|
||||
/// </summary>
|
||||
public class PositionComponent : IComponent
|
||||
{
|
||||
public Vector3 Position;
|
||||
}
|
||||
14
GameCore/Physics/VelocityComponent.cs
Normal file
14
GameCore/Physics/VelocityComponent.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Math;
|
||||
|
||||
namespace GameCore.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the 3D velocity of an entity.
|
||||
/// This component is used by the PhysicsSystem to update the PositionComponent.
|
||||
/// </summary>
|
||||
public class VelocityComponent : IComponent
|
||||
{
|
||||
public Vector3 ActualVelocity;
|
||||
public Vector3 DesiredVelocity;
|
||||
}
|
||||
7
GameCore/Player/PlayerComponent.cs
Normal file
7
GameCore/Player/PlayerComponent.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Player;
|
||||
|
||||
public class PlayerComponent : IComponent
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user