Initial import
This commit is contained in:
8
Assets/Scripts/Authoring.meta
Normal file
8
Assets/Scripts/Authoring.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a484a2375b573b4e9ed65298416f27e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
67
Assets/Scripts/Authoring/BuildingAuthoring.cs
Normal file
67
Assets/Scripts/Authoring/BuildingAuthoring.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
using EE2Clone.Components;
|
||||
using EE2Clone.Data;
|
||||
|
||||
namespace EE2Clone.Authoring
|
||||
{
|
||||
public class BuildingAuthoring : MonoBehaviour
|
||||
{
|
||||
public BuildingDataSO BuildingData;
|
||||
public bool StartConstructed = false;
|
||||
|
||||
public class Baker : Baker<BuildingAuthoring>
|
||||
{
|
||||
public override void Bake(BuildingAuthoring authoring)
|
||||
{
|
||||
var data = authoring.BuildingData;
|
||||
if (data == null) return;
|
||||
|
||||
var entity = GetEntity(TransformUsageFlags.Dynamic);
|
||||
|
||||
AddComponent(entity, new BuildingTag());
|
||||
AddComponent(entity, new Health
|
||||
{
|
||||
Current = authoring.StartConstructed ? data.MaxHealth : 1,
|
||||
Max = data.MaxHealth
|
||||
});
|
||||
AddComponent(entity, new OwnerPlayer { PlayerId = 0 });
|
||||
AddComponent(entity, new BuildingTypeComponent { Value = data.BuildingType });
|
||||
AddComponent(entity, new LineOfSight { Range = data.LineOfSightRange });
|
||||
AddComponent(entity, new EpochLevel { Value = data.RequiredEpoch });
|
||||
|
||||
if (!authoring.StartConstructed)
|
||||
{
|
||||
AddComponent(entity, new UnderConstructionTag());
|
||||
AddComponent(entity, new ConstructionProgress
|
||||
{
|
||||
Progress = 0f,
|
||||
BuildTime = data.BuildTime
|
||||
});
|
||||
}
|
||||
|
||||
if (data.TerritoryRadius > 0)
|
||||
{
|
||||
AddComponent(entity, new TerritorySource { Radius = data.TerritoryRadius });
|
||||
}
|
||||
|
||||
if (data.ProvidesPopulation > 0)
|
||||
{
|
||||
AddComponent(entity, new ProvidesPopulation { Amount = data.ProvidesPopulation });
|
||||
}
|
||||
|
||||
AddComponent(entity, new RallyPoint { Position = default });
|
||||
|
||||
// Production queue
|
||||
AddBuffer<ProductionQueueElement>(entity);
|
||||
AddBuffer<ResearchQueueElement>(entity);
|
||||
|
||||
// Dropoff
|
||||
if (data.IsDropoff && data.AcceptedResourceTypes != null && data.AcceptedResourceTypes.Length > 0)
|
||||
{
|
||||
AddComponent(entity, new DropoffBuilding { AcceptedType = data.AcceptedResourceTypes[0] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Authoring/BuildingAuthoring.cs.meta
Normal file
2
Assets/Scripts/Authoring/BuildingAuthoring.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37310523fb4035f4b96ce9b3d32af1f3
|
||||
44
Assets/Scripts/Authoring/PlayerStateAuthoring.cs
Normal file
44
Assets/Scripts/Authoring/PlayerStateAuthoring.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
using EE2Clone.Components;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Authoring
|
||||
{
|
||||
public class PlayerStateAuthoring : MonoBehaviour
|
||||
{
|
||||
public int StartingFood = 200;
|
||||
public int StartingWood = 200;
|
||||
public int StartingStone = 100;
|
||||
public int StartingGold = 100;
|
||||
|
||||
public class Baker : Baker<PlayerStateAuthoring>
|
||||
{
|
||||
public override void Bake(PlayerStateAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(TransformUsageFlags.None);
|
||||
|
||||
AddComponent(entity, new PlayerStateComponent
|
||||
{
|
||||
PlayerId = 0,
|
||||
CurrentEpoch = Epoch.StoneAge,
|
||||
PopulationCurrent = 0,
|
||||
PopulationMax = GameConstants.StartingPopulationCap,
|
||||
CivilizationId = 0,
|
||||
IsAlive = true
|
||||
});
|
||||
|
||||
AddComponent(entity, new PlayerResourcesComponent
|
||||
{
|
||||
Food = authoring.StartingFood,
|
||||
Wood = authoring.StartingWood,
|
||||
Stone = authoring.StartingStone,
|
||||
Gold = authoring.StartingGold,
|
||||
Tin = 0
|
||||
});
|
||||
|
||||
AddBuffer<PlayerTechBufferElement>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Authoring/PlayerStateAuthoring.cs.meta
Normal file
2
Assets/Scripts/Authoring/PlayerStateAuthoring.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78cb57b57a35e8e44951d464cc24dfab
|
||||
28
Assets/Scripts/Authoring/ProjectileAuthoring.cs
Normal file
28
Assets/Scripts/Authoring/ProjectileAuthoring.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
using EE2Clone.Components;
|
||||
|
||||
namespace EE2Clone.Authoring
|
||||
{
|
||||
public class ProjectileAuthoring : MonoBehaviour
|
||||
{
|
||||
public float Speed = 20f;
|
||||
|
||||
public class Baker : Baker<ProjectileAuthoring>
|
||||
{
|
||||
public override void Bake(ProjectileAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(TransformUsageFlags.Dynamic);
|
||||
|
||||
AddComponent(entity, new ProjectileTag());
|
||||
AddComponent(entity, new ProjectileData
|
||||
{
|
||||
Target = Entity.Null,
|
||||
Damage = 0,
|
||||
Speed = authoring.Speed,
|
||||
OwnerPlayerId = 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Authoring/ProjectileAuthoring.cs.meta
Normal file
2
Assets/Scripts/Authoring/ProjectileAuthoring.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6647a589e575c94788124df33f6dffd
|
||||
28
Assets/Scripts/Authoring/ResourceNodeAuthoring.cs
Normal file
28
Assets/Scripts/Authoring/ResourceNodeAuthoring.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
using EE2Clone.Components;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Authoring
|
||||
{
|
||||
public class ResourceNodeAuthoring : MonoBehaviour
|
||||
{
|
||||
public ResourceType ResourceType = ResourceType.Food;
|
||||
public int StartingAmount = 500;
|
||||
|
||||
public class Baker : Baker<ResourceNodeAuthoring>
|
||||
{
|
||||
public override void Bake(ResourceNodeAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(TransformUsageFlags.Dynamic);
|
||||
|
||||
AddComponent(entity, new ResourceNodeTag());
|
||||
AddComponent(entity, new ResourceNode
|
||||
{
|
||||
Type = authoring.ResourceType,
|
||||
RemainingAmount = authoring.StartingAmount
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Authoring/ResourceNodeAuthoring.cs.meta
Normal file
2
Assets/Scripts/Authoring/ResourceNodeAuthoring.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c7d47019d3edcc44b3e904abfe37c5c
|
||||
72
Assets/Scripts/Authoring/UnitAuthoring.cs
Normal file
72
Assets/Scripts/Authoring/UnitAuthoring.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
using EE2Clone.Components;
|
||||
using EE2Clone.Core;
|
||||
using EE2Clone.Data;
|
||||
|
||||
namespace EE2Clone.Authoring
|
||||
{
|
||||
public class UnitAuthoring : MonoBehaviour
|
||||
{
|
||||
public UnitDataSO UnitData;
|
||||
|
||||
public class Baker : Baker<UnitAuthoring>
|
||||
{
|
||||
public override void Bake(UnitAuthoring authoring)
|
||||
{
|
||||
var data = authoring.UnitData;
|
||||
if (data == null) return;
|
||||
|
||||
var entity = GetEntity(TransformUsageFlags.Dynamic);
|
||||
|
||||
AddComponent(entity, new UnitTag());
|
||||
AddComponent(entity, new Health { Current = data.MaxHealth, Max = data.MaxHealth });
|
||||
AddComponent(entity, new OwnerPlayer { PlayerId = 0 });
|
||||
AddComponent(entity, new MovementSpeed { Value = data.MoveSpeed });
|
||||
AddComponent(entity, new MoveTarget { Position = default, IsActive = false });
|
||||
AddComponent(entity, new AttackData
|
||||
{
|
||||
Damage = data.AttackDamage,
|
||||
Range = data.AttackRange,
|
||||
AttackCooldown = data.AttackCooldown,
|
||||
CooldownRemaining = 0
|
||||
});
|
||||
AddComponent(entity, new ArmorData { Value = data.Armor });
|
||||
AddComponent(entity, new UnitClassComponent { Value = data.UnitClass });
|
||||
AddComponent(entity, new UnitStateComponent { Value = UnitState.Idle });
|
||||
AddComponent(entity, new LineOfSight { Range = data.LineOfSightRange });
|
||||
AddComponent(entity, new EpochLevel { Value = data.RequiredEpoch });
|
||||
AddComponent(entity, new CombatTarget { Target = Entity.Null });
|
||||
|
||||
// Counter bonuses
|
||||
var counterBuffer = AddBuffer<CounterBonusElement>(entity);
|
||||
if (data.CounterBonuses != null)
|
||||
{
|
||||
foreach (var bonus in data.CounterBonuses)
|
||||
{
|
||||
counterBuffer.Add(new CounterBonusElement
|
||||
{
|
||||
TargetClass = bonus.TargetClass,
|
||||
Multiplier = bonus.Multiplier
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Citizen-specific components
|
||||
if (data.UnitClass == UnitClass.Citizen)
|
||||
{
|
||||
AddComponent(entity, new CitizenTag());
|
||||
AddComponent(entity, new CitizenStateComponent { Value = CitizenState.Idle });
|
||||
AddComponent(entity, new CarriedResource
|
||||
{
|
||||
Type = ResourceType.Food,
|
||||
Amount = 0,
|
||||
MaxCarryCapacity = data.MaxCarryCapacity
|
||||
});
|
||||
AddComponent(entity, new GatherTarget { Target = Entity.Null });
|
||||
AddComponent(entity, new BuildTarget { Target = Entity.Null });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Authoring/UnitAuthoring.cs.meta
Normal file
2
Assets/Scripts/Authoring/UnitAuthoring.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1bbeae99680553f46b1d3fd5c27d6342
|
||||
8
Assets/Scripts/Components.meta
Normal file
8
Assets/Scripts/Components.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 255ecf90be61ec741848f7d763680759
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
45
Assets/Scripts/Components/Aspects.cs
Normal file
45
Assets/Scripts/Components/Aspects.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace EE2Clone.Components
|
||||
{
|
||||
public readonly partial struct UnitAspect : IAspect
|
||||
{
|
||||
public readonly Entity Entity;
|
||||
|
||||
public readonly RefRW<LocalTransform> Transform;
|
||||
public readonly RefRO<Health> Health;
|
||||
public readonly RefRO<OwnerPlayer> Owner;
|
||||
public readonly RefRO<MovementSpeed> Speed;
|
||||
public readonly RefRW<MoveTarget> MoveTarget;
|
||||
public readonly RefRO<AttackData> Attack;
|
||||
public readonly RefRO<ArmorData> Armor;
|
||||
public readonly RefRO<UnitClassComponent> UnitClass;
|
||||
public readonly RefRW<UnitStateComponent> State;
|
||||
public readonly RefRO<LineOfSight> LineOfSight;
|
||||
}
|
||||
|
||||
public readonly partial struct BuildingAspect : IAspect
|
||||
{
|
||||
public readonly Entity Entity;
|
||||
|
||||
public readonly RefRO<LocalTransform> Transform;
|
||||
public readonly RefRW<Health> Health;
|
||||
public readonly RefRO<OwnerPlayer> Owner;
|
||||
public readonly RefRO<BuildingTypeComponent> BuildingType;
|
||||
public readonly RefRO<LineOfSight> LineOfSight;
|
||||
}
|
||||
|
||||
public readonly partial struct GathererAspect : IAspect
|
||||
{
|
||||
public readonly Entity Entity;
|
||||
|
||||
public readonly RefRW<LocalTransform> Transform;
|
||||
public readonly RefRO<OwnerPlayer> Owner;
|
||||
public readonly RefRO<MovementSpeed> Speed;
|
||||
public readonly RefRW<MoveTarget> MoveTarget;
|
||||
public readonly RefRW<CitizenStateComponent> CitizenState;
|
||||
public readonly RefRW<CarriedResource> CarriedResource;
|
||||
public readonly RefRW<GatherTarget> GatherTarget;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Components/Aspects.cs.meta
Normal file
2
Assets/Scripts/Components/Aspects.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d3da0d6cb90d294287efd6c1fa55c9f
|
||||
39
Assets/Scripts/Components/BufferComponents.cs
Normal file
39
Assets/Scripts/Components/BufferComponents.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Components
|
||||
{
|
||||
[GhostComponent]
|
||||
[InternalBufferCapacity(4)]
|
||||
public struct CounterBonusElement : IBufferElementData
|
||||
{
|
||||
[GhostField] public UnitClass TargetClass;
|
||||
[GhostField] public float Multiplier;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
[InternalBufferCapacity(5)]
|
||||
public struct ProductionQueueElement : IBufferElementData
|
||||
{
|
||||
[GhostField] public int UnitDataId;
|
||||
[GhostField] public float TimeRemaining;
|
||||
[GhostField] public float TotalTime;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
[InternalBufferCapacity(2)]
|
||||
public struct ResearchQueueElement : IBufferElementData
|
||||
{
|
||||
[GhostField] public int TechId;
|
||||
[GhostField] public float TimeRemaining;
|
||||
[GhostField] public float TotalTime;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
[InternalBufferCapacity(32)]
|
||||
public struct PlayerTechBufferElement : IBufferElementData
|
||||
{
|
||||
[GhostField] public int TechId;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Components/BufferComponents.cs.meta
Normal file
2
Assets/Scripts/Components/BufferComponents.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3fc0cacffc268b4f966956f308fbc31
|
||||
168
Assets/Scripts/Components/GameplayComponents.cs
Normal file
168
Assets/Scripts/Components/GameplayComponents.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Components
|
||||
{
|
||||
[GhostComponent]
|
||||
public struct Health : IComponentData
|
||||
{
|
||||
[GhostField] public float Current;
|
||||
[GhostField] public float Max;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct OwnerPlayer : IComponentData
|
||||
{
|
||||
[GhostField] public int PlayerId;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct MovementSpeed : IComponentData
|
||||
{
|
||||
[GhostField] public float Value;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct MoveTarget : IComponentData
|
||||
{
|
||||
[GhostField] public float3 Position;
|
||||
[GhostField] public bool IsActive;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct AttackData : IComponentData
|
||||
{
|
||||
[GhostField] public float Damage;
|
||||
[GhostField] public float Range;
|
||||
[GhostField] public float AttackCooldown;
|
||||
public float CooldownRemaining;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct ArmorData : IComponentData
|
||||
{
|
||||
[GhostField] public float Value;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct UnitClassComponent : IComponentData
|
||||
{
|
||||
[GhostField] public UnitClass Value;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct UnitStateComponent : IComponentData
|
||||
{
|
||||
[GhostField] public UnitState Value;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct ResourceNode : IComponentData
|
||||
{
|
||||
[GhostField] public ResourceType Type;
|
||||
[GhostField] public int RemainingAmount;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct CarriedResource : IComponentData
|
||||
{
|
||||
[GhostField] public ResourceType Type;
|
||||
[GhostField] public int Amount;
|
||||
[GhostField] public int MaxCarryCapacity;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct GatherTarget : IComponentData
|
||||
{
|
||||
[GhostField] public Entity Target;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct BuildTarget : IComponentData
|
||||
{
|
||||
[GhostField] public Entity Target;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct ConstructionProgress : IComponentData
|
||||
{
|
||||
[GhostField] public float Progress; // 0.0 to 1.0
|
||||
[GhostField] public float BuildTime; // Total time required
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct ProductionTimer : IComponentData
|
||||
{
|
||||
[GhostField] public float TimeRemaining;
|
||||
[GhostField] public float TotalTime;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct RallyPoint : IComponentData
|
||||
{
|
||||
[GhostField] public float3 Position;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct TerritorySource : IComponentData
|
||||
{
|
||||
[GhostField] public float Radius;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct LineOfSight : IComponentData
|
||||
{
|
||||
[GhostField] public float Range;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct EpochLevel : IComponentData
|
||||
{
|
||||
[GhostField] public Epoch Value;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct CitizenStateComponent : IComponentData
|
||||
{
|
||||
[GhostField] public CitizenState Value;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct DropoffBuilding : IComponentData
|
||||
{
|
||||
[GhostField] public ResourceType AcceptedType;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct CombatTarget : IComponentData
|
||||
{
|
||||
[GhostField] public Entity Target;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct BuildingTypeComponent : IComponentData
|
||||
{
|
||||
[GhostField] public BuildingType Value;
|
||||
}
|
||||
|
||||
[GhostComponent]
|
||||
public struct ProvidesPopulation : IComponentData
|
||||
{
|
||||
[GhostField] public int Amount;
|
||||
}
|
||||
|
||||
public struct ProjectileData : IComponentData
|
||||
{
|
||||
[GhostField] public Entity Target;
|
||||
[GhostField] public float Damage;
|
||||
[GhostField] public float Speed;
|
||||
[GhostField] public int OwnerPlayerId;
|
||||
}
|
||||
|
||||
public struct DeathTimer : IComponentData
|
||||
{
|
||||
public float TimeRemaining;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Components/GameplayComponents.cs.meta
Normal file
2
Assets/Scripts/Components/GameplayComponents.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43034d84c3a5fa249b16c70c205c781d
|
||||
61
Assets/Scripts/Components/PlayerComponents.cs
Normal file
61
Assets/Scripts/Components/PlayerComponents.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Components
|
||||
{
|
||||
[GhostComponent(PrefabType = GhostPrefabType.All)]
|
||||
public struct PlayerStateComponent : IComponentData
|
||||
{
|
||||
[GhostField] public int PlayerId;
|
||||
[GhostField] public Epoch CurrentEpoch;
|
||||
[GhostField] public int PopulationCurrent;
|
||||
[GhostField] public int PopulationMax;
|
||||
[GhostField] public int CivilizationId;
|
||||
[GhostField] public bool IsAlive;
|
||||
}
|
||||
|
||||
[GhostComponent(PrefabType = GhostPrefabType.All)]
|
||||
public struct PlayerResourcesComponent : IComponentData
|
||||
{
|
||||
[GhostField] public int Food;
|
||||
[GhostField] public int Wood;
|
||||
[GhostField] public int Stone;
|
||||
[GhostField] public int Gold;
|
||||
[GhostField] public int Tin;
|
||||
|
||||
public int GetResource(ResourceType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ResourceType.Food => Food,
|
||||
ResourceType.Wood => Wood,
|
||||
ResourceType.Stone => Stone,
|
||||
ResourceType.Gold => Gold,
|
||||
ResourceType.Tin => Tin,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
public void AddResource(ResourceType type, int amount)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ResourceType.Food: Food += amount; break;
|
||||
case ResourceType.Wood: Wood += amount; break;
|
||||
case ResourceType.Stone: Stone += amount; break;
|
||||
case ResourceType.Gold: Gold += amount; break;
|
||||
case ResourceType.Tin: Tin += amount; break;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TrySpend(ResourceType type, int amount)
|
||||
{
|
||||
if (GetResource(type) < amount)
|
||||
return false;
|
||||
|
||||
AddResource(type, -amount);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Components/PlayerComponents.cs.meta
Normal file
2
Assets/Scripts/Components/PlayerComponents.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f66e6e599792edc4594f96c856bd7ccd
|
||||
22
Assets/Scripts/Components/Tags.cs
Normal file
22
Assets/Scripts/Components/Tags.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace EE2Clone.Components
|
||||
{
|
||||
public struct UnitTag : IComponentData { }
|
||||
public struct BuildingTag : IComponentData { }
|
||||
public struct CitizenTag : IComponentData { }
|
||||
public struct ProjectileTag : IComponentData { }
|
||||
public struct ResourceNodeTag : IComponentData { }
|
||||
public struct SelectedTag : IComponentData { }
|
||||
public struct DestroyEntityTag : IComponentData { }
|
||||
|
||||
/// <summary>
|
||||
/// Marks an entity as under construction (not yet functional).
|
||||
/// </summary>
|
||||
public struct UnderConstructionTag : IComponentData { }
|
||||
|
||||
/// <summary>
|
||||
/// Tag for entities that have just spawned and need initialization.
|
||||
/// </summary>
|
||||
public struct NewlySpawnedTag : IComponentData { }
|
||||
}
|
||||
2
Assets/Scripts/Components/Tags.cs.meta
Normal file
2
Assets/Scripts/Components/Tags.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38e1791fd078c6a4c923d7fdb0d81da1
|
||||
8
Assets/Scripts/Core.meta
Normal file
8
Assets/Scripts/Core.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 584693cd698b6434ea5da7d837864661
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
90
Assets/Scripts/Core/GameConstants.cs
Normal file
90
Assets/Scripts/Core/GameConstants.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
namespace EE2Clone.Core
|
||||
{
|
||||
public enum UnitClass
|
||||
{
|
||||
Citizen,
|
||||
Infantry,
|
||||
Ranged,
|
||||
Cavalry,
|
||||
Siege,
|
||||
Priest
|
||||
}
|
||||
|
||||
public enum ResourceType
|
||||
{
|
||||
Food,
|
||||
Wood,
|
||||
Stone,
|
||||
Gold,
|
||||
Tin
|
||||
}
|
||||
|
||||
public enum Epoch
|
||||
{
|
||||
StoneAge = 0,
|
||||
BronzeAge = 1,
|
||||
IronAge = 2
|
||||
}
|
||||
|
||||
public enum BuildingType
|
||||
{
|
||||
TownCenter,
|
||||
House,
|
||||
Barracks,
|
||||
ArcheryRange,
|
||||
Stable,
|
||||
SiegeWorkshop,
|
||||
Temple,
|
||||
University,
|
||||
Farm,
|
||||
LumberCamp,
|
||||
MiningCamp,
|
||||
Quarry,
|
||||
Wall,
|
||||
Tower
|
||||
}
|
||||
|
||||
public enum CitizenState
|
||||
{
|
||||
Idle,
|
||||
MovingToGather,
|
||||
Gathering,
|
||||
MovingToDropoff,
|
||||
Depositing,
|
||||
MovingToBuild,
|
||||
Building,
|
||||
Repairing,
|
||||
Fighting
|
||||
}
|
||||
|
||||
public enum UnitState
|
||||
{
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Dying
|
||||
}
|
||||
|
||||
public static class GameConstants
|
||||
{
|
||||
public const int MaxPlayers = 8;
|
||||
public const int MaxPopulationCap = 200;
|
||||
public const int PopulationPerHouse = 5;
|
||||
public const int StartingPopulationCap = 10;
|
||||
|
||||
public const float GatherTickInterval = 1.0f;
|
||||
public const float ConstructionTickInterval = 0.5f;
|
||||
|
||||
public const int FlowFieldWidth = 256;
|
||||
public const int FlowFieldHeight = 256;
|
||||
public const float FlowFieldCellSize = 1.0f;
|
||||
|
||||
public const float MinDamage = 1f;
|
||||
|
||||
// Counter bonus multipliers (rock-paper-scissors)
|
||||
public const float CounterBonusStrong = 1.5f;
|
||||
public const float CounterBonusWeak = 0.75f;
|
||||
public const float CounterBonusNeutral = 1.0f;
|
||||
public const float SiegeBuildingBonus = 3.0f;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Core/GameConstants.cs.meta
Normal file
2
Assets/Scripts/Core/GameConstants.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 986c0d6aa51063547b8a637d1a4b4986
|
||||
8
Assets/Scripts/Data.meta
Normal file
8
Assets/Scripts/Data.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f00ac55dfbdf8748b594e18877c9c5b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
47
Assets/Scripts/Data/BuildingDataSO.cs
Normal file
47
Assets/Scripts/Data/BuildingDataSO.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using UnityEngine;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Data
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewBuildingData", menuName = "EE2Clone/Building Data")]
|
||||
public class BuildingDataSO : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
public int Id;
|
||||
public string BuildingName;
|
||||
public BuildingType BuildingType;
|
||||
public Epoch RequiredEpoch;
|
||||
|
||||
[Header("Stats")]
|
||||
public float MaxHealth = 500;
|
||||
public float LineOfSightRange = 8f;
|
||||
public float BuildTime = 20f;
|
||||
|
||||
[Header("Territory")]
|
||||
public float TerritoryRadius;
|
||||
|
||||
[Header("Population")]
|
||||
public int ProvidesPopulation;
|
||||
|
||||
[Header("Resource Dropoff")]
|
||||
public bool IsDropoff;
|
||||
public ResourceType[] AcceptedResourceTypes;
|
||||
|
||||
[Header("Production")]
|
||||
public int[] ProducibleUnitIds;
|
||||
public int[] ResearchableTechIds;
|
||||
|
||||
[Header("Cost")]
|
||||
public int FoodCost;
|
||||
public int WoodCost;
|
||||
public int StoneCost;
|
||||
public int GoldCost;
|
||||
public int TinCost;
|
||||
|
||||
[Header("Footprint")]
|
||||
public Vector2Int FootprintSize = new(3, 3);
|
||||
|
||||
[Header("Visuals")]
|
||||
public GameObject Prefab;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Data/BuildingDataSO.cs.meta
Normal file
2
Assets/Scripts/Data/BuildingDataSO.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba7586487d870fb4dbfd31fee37961d4
|
||||
27
Assets/Scripts/Data/CivilizationDataSO.cs
Normal file
27
Assets/Scripts/Data/CivilizationDataSO.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace EE2Clone.Data
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewCivilizationData", menuName = "EE2Clone/Civilization Data")]
|
||||
public class CivilizationDataSO : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
public int Id;
|
||||
public string CivilizationName;
|
||||
public string Description;
|
||||
|
||||
[Header("Starting Bonuses")]
|
||||
public int BonusFood;
|
||||
public int BonusWood;
|
||||
public int BonusStone;
|
||||
public int BonusGold;
|
||||
|
||||
[Header("Stat Modifiers")]
|
||||
public float InfantryAttackModifier = 1f;
|
||||
public float RangedAttackModifier = 1f;
|
||||
public float CavalryAttackModifier = 1f;
|
||||
public float GatherSpeedModifier = 1f;
|
||||
public float BuildSpeedModifier = 1f;
|
||||
public float ResearchSpeedModifier = 1f;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Data/CivilizationDataSO.cs.meta
Normal file
2
Assets/Scripts/Data/CivilizationDataSO.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5e0e074c1ddf1743bdf4ea4688e7441
|
||||
28
Assets/Scripts/Data/EpochDataSO.cs
Normal file
28
Assets/Scripts/Data/EpochDataSO.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using UnityEngine;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Data
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewEpochData", menuName = "EE2Clone/Epoch Data")]
|
||||
public class EpochDataSO : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
public Epoch Epoch;
|
||||
public string EpochName;
|
||||
public string Description;
|
||||
|
||||
[Header("Advance Requirements")]
|
||||
public int FoodCost;
|
||||
public int WoodCost;
|
||||
public int StoneCost;
|
||||
public int GoldCost;
|
||||
public int TinCost;
|
||||
public int[] RequiredTechIds;
|
||||
public float AdvanceTime = 60f;
|
||||
|
||||
[Header("Unlocks")]
|
||||
public int[] UnlockedBuildingIds;
|
||||
public int[] UnlockedUnitIds;
|
||||
public int[] UnlockedTechIds;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Data/EpochDataSO.cs.meta
Normal file
2
Assets/Scripts/Data/EpochDataSO.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a02854ba574df84d84820e3f8feb651
|
||||
48
Assets/Scripts/Data/TechDataSO.cs
Normal file
48
Assets/Scripts/Data/TechDataSO.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using UnityEngine;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Data
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewTechData", menuName = "EE2Clone/Tech Data")]
|
||||
public class TechDataSO : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
public int Id;
|
||||
public string TechName;
|
||||
public string Description;
|
||||
public Epoch RequiredEpoch;
|
||||
|
||||
[Header("Prerequisites")]
|
||||
public int[] PrerequisiteTechIds;
|
||||
|
||||
[Header("Research")]
|
||||
public float ResearchTime = 30f;
|
||||
public int FoodCost;
|
||||
public int WoodCost;
|
||||
public int StoneCost;
|
||||
public int GoldCost;
|
||||
public int TinCost;
|
||||
|
||||
[Header("Effects")]
|
||||
public TechEffect[] Effects;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public struct TechEffect
|
||||
{
|
||||
public TechEffectType EffectType;
|
||||
public UnitClass AffectedUnitClass;
|
||||
public float Value;
|
||||
}
|
||||
|
||||
public enum TechEffectType
|
||||
{
|
||||
AttackBonus,
|
||||
ArmorBonus,
|
||||
SpeedBonus,
|
||||
HealthBonus,
|
||||
GatherSpeedBonus,
|
||||
BuildSpeedBonus,
|
||||
TrainSpeedBonus
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Data/TechDataSO.cs.meta
Normal file
2
Assets/Scripts/Data/TechDataSO.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 93cdf2b99ef6a4340ab0635822ff6b95
|
||||
51
Assets/Scripts/Data/UnitDataSO.cs
Normal file
51
Assets/Scripts/Data/UnitDataSO.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using UnityEngine;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Data
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewUnitData", menuName = "EE2Clone/Unit Data")]
|
||||
public class UnitDataSO : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
public int Id;
|
||||
public string UnitName;
|
||||
public UnitClass UnitClass;
|
||||
public Epoch RequiredEpoch;
|
||||
|
||||
[Header("Stats")]
|
||||
public float MaxHealth = 100;
|
||||
public float MoveSpeed = 4f;
|
||||
public float AttackDamage = 10f;
|
||||
public float AttackRange = 1.5f;
|
||||
public float AttackCooldown = 1f;
|
||||
public float Armor = 1f;
|
||||
public float LineOfSightRange = 10f;
|
||||
|
||||
[Header("Gathering (Citizens only)")]
|
||||
public int MaxCarryCapacity = 10;
|
||||
public float GatherSpeed = 1f;
|
||||
public float BuildSpeed = 1f;
|
||||
|
||||
[Header("Cost")]
|
||||
public int FoodCost;
|
||||
public int WoodCost;
|
||||
public int StoneCost;
|
||||
public int GoldCost;
|
||||
public int TinCost;
|
||||
public float TrainTime = 10f;
|
||||
public int PopulationCost = 1;
|
||||
|
||||
[Header("Counter Bonuses")]
|
||||
public CounterBonus[] CounterBonuses;
|
||||
|
||||
[Header("Visuals")]
|
||||
public GameObject Prefab;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public struct CounterBonus
|
||||
{
|
||||
public UnitClass TargetClass;
|
||||
public float Multiplier;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Data/UnitDataSO.cs.meta
Normal file
2
Assets/Scripts/Data/UnitDataSO.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c618988cd6fa6fe43a54b0e26bfd691d
|
||||
24
Assets/Scripts/EE2Clone.asmdef
Normal file
24
Assets/Scripts/EE2Clone.asmdef
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "EE2Clone",
|
||||
"rootNamespace": "EE2Clone",
|
||||
"references": [
|
||||
"Unity.Entities",
|
||||
"Unity.Entities.Hybrid",
|
||||
"Unity.Transforms",
|
||||
"Unity.Mathematics",
|
||||
"Unity.Collections",
|
||||
"Unity.Burst",
|
||||
"Unity.Physics",
|
||||
"Unity.NetCode",
|
||||
"Unity.InputSystem"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
7
Assets/Scripts/EE2Clone.asmdef.meta
Normal file
7
Assets/Scripts/EE2Clone.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d4b91ddf61b827459f2f7d2db612f5b
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Hybrid.meta
Normal file
8
Assets/Scripts/Hybrid.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eeceb2808c5c7f04e9a5883989ed3bc2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
219
Assets/Scripts/Hybrid/CommandDispatcher.cs
Normal file
219
Assets/Scripts/Hybrid/CommandDispatcher.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
using Unity.Collections;
|
||||
using Unity.Physics;
|
||||
using UnityEngine;
|
||||
using EE2Clone.Components;
|
||||
using EE2Clone.NetCode;
|
||||
|
||||
namespace EE2Clone.Hybrid
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes right-click context commands: move, attack, gather, build/repair.
|
||||
/// Sends the appropriate RPC to the server based on what was right-clicked.
|
||||
/// Client-side MonoBehaviour.
|
||||
/// </summary>
|
||||
public class CommandDispatcher : MonoBehaviour
|
||||
{
|
||||
private Camera _mainCamera;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_mainCamera = Camera.main;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var input = RTSInputActions.Instance;
|
||||
if (input == null || _mainCamera == null) return;
|
||||
|
||||
if (input.RightClickPressed)
|
||||
HandleRightClick(input.MousePosition);
|
||||
|
||||
if (input.StopPressed)
|
||||
HandleStop();
|
||||
}
|
||||
|
||||
private void HandleRightClick(Vector2 screenPos)
|
||||
{
|
||||
var selection = SelectionManager.Instance;
|
||||
if (selection == null || selection.SelectedEntities.Count == 0) return;
|
||||
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null || !world.IsCreated) return;
|
||||
|
||||
var em = world.EntityManager;
|
||||
var ray = _mainCamera.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0));
|
||||
|
||||
// Raycast to find what was clicked
|
||||
var physicsWorldQuery = em.CreateEntityQuery(typeof(PhysicsWorldSingleton));
|
||||
if (physicsWorldQuery.IsEmpty) return;
|
||||
|
||||
var physicsWorld = physicsWorldQuery.GetSingleton<PhysicsWorldSingleton>();
|
||||
var rayInput = new RaycastInput
|
||||
{
|
||||
Start = ray.origin,
|
||||
End = ray.origin + ray.direction * 500f,
|
||||
Filter = new CollisionFilter
|
||||
{
|
||||
BelongsTo = ~0u,
|
||||
CollidesWith = ~0u,
|
||||
GroupIndex = 0
|
||||
}
|
||||
};
|
||||
|
||||
Entity hitEntity = Entity.Null;
|
||||
float3 hitPosition = float3.zero;
|
||||
|
||||
if (physicsWorld.CastRay(rayInput, out var hit))
|
||||
{
|
||||
hitEntity = physicsWorld.Bodies[hit.RigidBodyIndex].Entity;
|
||||
hitPosition = hit.Position;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No hit — use ground plane intersection
|
||||
if (ray.direction.y != 0)
|
||||
{
|
||||
float t = -ray.origin.y / ray.direction.y;
|
||||
if (t > 0)
|
||||
hitPosition = new float3(ray.origin.x + ray.direction.x * t, 0, ray.origin.z + ray.direction.z * t);
|
||||
}
|
||||
return; // Only move command if no entity hit
|
||||
}
|
||||
|
||||
// Determine command type based on what was clicked
|
||||
if (hitEntity != Entity.Null && em.Exists(hitEntity))
|
||||
{
|
||||
if (em.HasComponent<ResourceNodeTag>(hitEntity))
|
||||
{
|
||||
// Gather command for citizens
|
||||
SendGatherCommands(hitEntity);
|
||||
return;
|
||||
}
|
||||
|
||||
if (em.HasComponent<UnitTag>(hitEntity) || em.HasComponent<BuildingTag>(hitEntity))
|
||||
{
|
||||
if (em.HasComponent<OwnerPlayer>(hitEntity))
|
||||
{
|
||||
var owner = em.GetComponentData<OwnerPlayer>(hitEntity);
|
||||
// TODO: compare with local player id
|
||||
// For now, treat as enemy → attack
|
||||
SendAttackCommands(hitEntity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (em.HasComponent<BuildingTag>(hitEntity) && em.HasComponent<UnderConstructionTag>(hitEntity))
|
||||
{
|
||||
// Build/repair command for citizens
|
||||
SendBuildRepairCommands(hitEntity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Default: move command
|
||||
SendMoveCommands(hitPosition);
|
||||
}
|
||||
|
||||
private void SendMoveCommands(float3 position)
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null) return;
|
||||
var em = world.EntityManager;
|
||||
|
||||
foreach (var unit in SelectionManager.Instance.SelectedEntities)
|
||||
{
|
||||
if (!em.Exists(unit) || !em.HasComponent<UnitTag>(unit)) continue;
|
||||
|
||||
var rpcEntity = em.CreateEntity();
|
||||
em.AddComponentData(rpcEntity, new MoveCommandRpc
|
||||
{
|
||||
UnitEntity = unit,
|
||||
TargetPosition = position
|
||||
});
|
||||
em.AddComponent<SendRpcCommandRequest>(rpcEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendAttackCommands(Entity target)
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null) return;
|
||||
var em = world.EntityManager;
|
||||
|
||||
foreach (var unit in SelectionManager.Instance.SelectedEntities)
|
||||
{
|
||||
if (!em.Exists(unit) || !em.HasComponent<UnitTag>(unit)) continue;
|
||||
|
||||
var rpcEntity = em.CreateEntity();
|
||||
em.AddComponentData(rpcEntity, new AttackCommandRpc
|
||||
{
|
||||
AttackerEntity = unit,
|
||||
TargetEntity = target
|
||||
});
|
||||
em.AddComponent<SendRpcCommandRequest>(rpcEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendGatherCommands(Entity resourceNode)
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null) return;
|
||||
var em = world.EntityManager;
|
||||
|
||||
foreach (var unit in SelectionManager.Instance.SelectedEntities)
|
||||
{
|
||||
if (!em.Exists(unit) || !em.HasComponent<CitizenTag>(unit)) continue;
|
||||
|
||||
var rpcEntity = em.CreateEntity();
|
||||
em.AddComponentData(rpcEntity, new GatherCommandRpc
|
||||
{
|
||||
CitizenEntity = unit,
|
||||
ResourceNodeEntity = resourceNode
|
||||
});
|
||||
em.AddComponent<SendRpcCommandRequest>(rpcEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendBuildRepairCommands(Entity building)
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null) return;
|
||||
var em = world.EntityManager;
|
||||
|
||||
foreach (var unit in SelectionManager.Instance.SelectedEntities)
|
||||
{
|
||||
if (!em.Exists(unit) || !em.HasComponent<CitizenTag>(unit)) continue;
|
||||
|
||||
var rpcEntity = em.CreateEntity();
|
||||
em.AddComponentData(rpcEntity, new BuildRepairCommandRpc
|
||||
{
|
||||
CitizenEntity = unit,
|
||||
BuildingEntity = building
|
||||
});
|
||||
em.AddComponent<SendRpcCommandRequest>(rpcEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleStop()
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null) return;
|
||||
var em = world.EntityManager;
|
||||
|
||||
foreach (var unit in SelectionManager.Instance.SelectedEntities)
|
||||
{
|
||||
if (!em.Exists(unit) || !em.HasComponent<UnitTag>(unit)) continue;
|
||||
|
||||
var rpcEntity = em.CreateEntity();
|
||||
em.AddComponentData(rpcEntity, new StopCommandRpc
|
||||
{
|
||||
UnitEntity = unit
|
||||
});
|
||||
em.AddComponent<SendRpcCommandRequest>(rpcEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Hybrid/CommandDispatcher.cs.meta
Normal file
2
Assets/Scripts/Hybrid/CommandDispatcher.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f074ad7027c0ccc409dcab5f7d5abbde
|
||||
106
Assets/Scripts/Hybrid/RTSCameraController.cs
Normal file
106
Assets/Scripts/Hybrid/RTSCameraController.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace EE2Clone.Hybrid
|
||||
{
|
||||
/// <summary>
|
||||
/// RTS-style camera controller. Supports WASD/arrow pan, edge-of-screen pan,
|
||||
/// scroll zoom, and Q/E rotation. Client-side only (MonoBehaviour).
|
||||
/// </summary>
|
||||
public class RTSCameraController : MonoBehaviour
|
||||
{
|
||||
[Header("Movement")]
|
||||
[SerializeField] private float panSpeed = 30f;
|
||||
[SerializeField] private float edgePanThreshold = 15f;
|
||||
[SerializeField] private bool enableEdgePan = true;
|
||||
|
||||
[Header("Zoom")]
|
||||
[SerializeField] private float zoomSpeed = 10f;
|
||||
[SerializeField] private float minZoomHeight = 10f;
|
||||
[SerializeField] private float maxZoomHeight = 80f;
|
||||
|
||||
[Header("Rotation")]
|
||||
[SerializeField] private float rotateSpeed = 100f;
|
||||
|
||||
[Header("Bounds")]
|
||||
[SerializeField] private Vector2 mapMin = new(-128f, -128f);
|
||||
[SerializeField] private Vector2 mapMax = new(128f, 128f);
|
||||
|
||||
private Transform _cameraTransform;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_cameraTransform = transform;
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
var input = RTSInputActions.Instance;
|
||||
if (input == null) return;
|
||||
|
||||
HandlePan(input);
|
||||
HandleZoom(input);
|
||||
HandleRotation(input);
|
||||
ClampPosition();
|
||||
}
|
||||
|
||||
private void HandlePan(RTSInputActions input)
|
||||
{
|
||||
var panInput = input.CameraPan;
|
||||
var panDir = new Vector3(panInput.x, 0f, panInput.y);
|
||||
|
||||
// Edge-of-screen panning
|
||||
if (enableEdgePan)
|
||||
{
|
||||
var mousePos = input.MousePosition;
|
||||
if (mousePos.x <= edgePanThreshold) panDir.x -= 1f;
|
||||
if (mousePos.x >= Screen.width - edgePanThreshold) panDir.x += 1f;
|
||||
if (mousePos.y <= edgePanThreshold) panDir.z -= 1f;
|
||||
if (mousePos.y >= Screen.height - edgePanThreshold) panDir.z += 1f;
|
||||
}
|
||||
|
||||
if (panDir.sqrMagnitude > 0.01f)
|
||||
{
|
||||
panDir.Normalize();
|
||||
// Move relative to camera's forward (ignoring Y)
|
||||
var forward = _cameraTransform.forward;
|
||||
forward.y = 0f;
|
||||
forward.Normalize();
|
||||
var right = _cameraTransform.right;
|
||||
right.y = 0f;
|
||||
right.Normalize();
|
||||
|
||||
var move = (forward * panDir.z + right * panDir.x) * (panSpeed * Time.unscaledDeltaTime);
|
||||
_cameraTransform.position += move;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleZoom(RTSInputActions input)
|
||||
{
|
||||
var zoomInput = input.CameraZoom;
|
||||
if (Mathf.Abs(zoomInput) > 0.01f)
|
||||
{
|
||||
var pos = _cameraTransform.position;
|
||||
pos.y -= zoomInput * zoomSpeed * Time.unscaledDeltaTime;
|
||||
pos.y = Mathf.Clamp(pos.y, minZoomHeight, maxZoomHeight);
|
||||
_cameraTransform.position = pos;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleRotation(RTSInputActions input)
|
||||
{
|
||||
var rotateInput = input.CameraRotate;
|
||||
if (Mathf.Abs(rotateInput) > 0.01f)
|
||||
{
|
||||
_cameraTransform.Rotate(Vector3.up, rotateInput * rotateSpeed * Time.unscaledDeltaTime, Space.World);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClampPosition()
|
||||
{
|
||||
var pos = _cameraTransform.position;
|
||||
pos.x = Mathf.Clamp(pos.x, mapMin.x, mapMax.x);
|
||||
pos.z = Mathf.Clamp(pos.z, mapMin.y, mapMax.y);
|
||||
_cameraTransform.position = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Hybrid/RTSCameraController.cs.meta
Normal file
2
Assets/Scripts/Hybrid/RTSCameraController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a2d497f840a0f54d823f815babd595c
|
||||
140
Assets/Scripts/Hybrid/RTSInputActions.cs
Normal file
140
Assets/Scripts/Hybrid/RTSInputActions.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace EE2Clone.Hybrid
|
||||
{
|
||||
/// <summary>
|
||||
/// MonoBehaviour that reads from the Input System and exposes RTS-specific input state
|
||||
/// for the hybrid camera, selection, and UI systems. This is the bridge between
|
||||
/// Unity's Input System and our ECS command pipeline.
|
||||
/// </summary>
|
||||
public class RTSInputActions : MonoBehaviour
|
||||
{
|
||||
public static RTSInputActions Instance { get; private set; }
|
||||
|
||||
[Header("Input Action References")]
|
||||
[SerializeField] private InputActionAsset inputActions;
|
||||
|
||||
// Camera
|
||||
public Vector2 CameraPan { get; private set; }
|
||||
public float CameraZoom { get; private set; }
|
||||
public float CameraRotate { get; private set; }
|
||||
|
||||
// Selection
|
||||
public bool LeftClickPressed { get; private set; }
|
||||
public bool LeftClickReleased { get; private set; }
|
||||
public bool LeftClickHeld { get; private set; }
|
||||
public bool RightClickPressed { get; private set; }
|
||||
public Vector2 MousePosition { get; private set; }
|
||||
|
||||
// Modifiers
|
||||
public bool ShiftHeld { get; private set; }
|
||||
public bool CtrlHeld { get; private set; }
|
||||
|
||||
// Actions
|
||||
public bool AttackMovePressed { get; private set; }
|
||||
public bool PatrolPressed { get; private set; }
|
||||
public bool StopPressed { get; private set; }
|
||||
public bool DeletePressed { get; private set; }
|
||||
|
||||
// Control groups (0-9)
|
||||
public int ControlGroupPressed { get; private set; } = -1;
|
||||
|
||||
private InputAction _cameraPanAction;
|
||||
private InputAction _cameraZoomAction;
|
||||
private InputAction _cameraRotateAction;
|
||||
private InputAction _leftClickAction;
|
||||
private InputAction _rightClickAction;
|
||||
private InputAction _mousePositionAction;
|
||||
private InputAction _shiftAction;
|
||||
private InputAction _ctrlAction;
|
||||
private InputAction _attackMoveAction;
|
||||
private InputAction _patrolAction;
|
||||
private InputAction _stopAction;
|
||||
private InputAction _deleteAction;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (inputActions == null) return;
|
||||
|
||||
var rtsMap = inputActions.FindActionMap("RTS");
|
||||
if (rtsMap == null)
|
||||
{
|
||||
Debug.LogError("RTS action map not found in Input Actions asset!");
|
||||
return;
|
||||
}
|
||||
|
||||
_cameraPanAction = rtsMap.FindAction("CameraPan");
|
||||
_cameraZoomAction = rtsMap.FindAction("CameraZoom");
|
||||
_cameraRotateAction = rtsMap.FindAction("CameraRotate");
|
||||
_leftClickAction = rtsMap.FindAction("LeftClick");
|
||||
_rightClickAction = rtsMap.FindAction("RightClick");
|
||||
_mousePositionAction = rtsMap.FindAction("MousePosition");
|
||||
_shiftAction = rtsMap.FindAction("Shift");
|
||||
_ctrlAction = rtsMap.FindAction("Ctrl");
|
||||
_attackMoveAction = rtsMap.FindAction("AttackMove");
|
||||
_patrolAction = rtsMap.FindAction("Patrol");
|
||||
_stopAction = rtsMap.FindAction("Stop");
|
||||
_deleteAction = rtsMap.FindAction("Delete");
|
||||
|
||||
rtsMap.Enable();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (inputActions == null) return;
|
||||
var rtsMap = inputActions.FindActionMap("RTS");
|
||||
rtsMap?.Disable();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
CameraPan = _cameraPanAction?.ReadValue<Vector2>() ?? Vector2.zero;
|
||||
CameraZoom = _cameraZoomAction?.ReadValue<float>() ?? 0f;
|
||||
CameraRotate = _cameraRotateAction?.ReadValue<float>() ?? 0f;
|
||||
|
||||
LeftClickPressed = _leftClickAction?.WasPressedThisFrame() ?? false;
|
||||
LeftClickReleased = _leftClickAction?.WasReleasedThisFrame() ?? false;
|
||||
LeftClickHeld = _leftClickAction?.IsPressed() ?? false;
|
||||
RightClickPressed = _rightClickAction?.WasPressedThisFrame() ?? false;
|
||||
|
||||
MousePosition = _mousePositionAction?.ReadValue<Vector2>() ?? Vector2.zero;
|
||||
|
||||
ShiftHeld = _shiftAction?.IsPressed() ?? false;
|
||||
CtrlHeld = _ctrlAction?.IsPressed() ?? false;
|
||||
|
||||
AttackMovePressed = _attackMoveAction?.WasPressedThisFrame() ?? false;
|
||||
PatrolPressed = _patrolAction?.WasPressedThisFrame() ?? false;
|
||||
StopPressed = _stopAction?.WasPressedThisFrame() ?? false;
|
||||
DeletePressed = _deleteAction?.WasPressedThisFrame() ?? false;
|
||||
|
||||
// Control groups (keyboard 0-9)
|
||||
ControlGroupPressed = -1;
|
||||
for (int i = 0; i <= 9; i++)
|
||||
{
|
||||
if (Keyboard.current != null && Keyboard.current[Key.Digit0 + i].wasPressedThisFrame)
|
||||
{
|
||||
ControlGroupPressed = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Hybrid/RTSInputActions.cs.meta
Normal file
2
Assets/Scripts/Hybrid/RTSInputActions.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad9c0e075e79563498b1577211b6e351
|
||||
67
Assets/Scripts/Hybrid/SelectionBoxUI.cs
Normal file
67
Assets/Scripts/Hybrid/SelectionBoxUI.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace EE2Clone.Hybrid
|
||||
{
|
||||
/// <summary>
|
||||
/// Draws the rubber-band selection box on screen when the player is drag-selecting.
|
||||
/// </summary>
|
||||
public class SelectionBoxUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Color boxColor = new(0.2f, 0.8f, 0.2f, 0.25f);
|
||||
[SerializeField] private Color borderColor = new(0.2f, 0.8f, 0.2f, 0.8f);
|
||||
|
||||
private Texture2D _fillTexture;
|
||||
private Texture2D _borderTexture;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_fillTexture = new Texture2D(1, 1);
|
||||
_fillTexture.SetPixel(0, 0, Color.white);
|
||||
_fillTexture.Apply();
|
||||
|
||||
_borderTexture = new Texture2D(1, 1);
|
||||
_borderTexture.SetPixel(0, 0, Color.white);
|
||||
_borderTexture.Apply();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
var sel = SelectionManager.Instance;
|
||||
if (sel == null || !sel.IsDragging) return;
|
||||
|
||||
var start = sel.DragStart;
|
||||
var end = sel.DragEnd;
|
||||
|
||||
// Convert to GUI coordinates (Y is flipped)
|
||||
start.y = Screen.height - start.y;
|
||||
end.y = Screen.height - end.y;
|
||||
|
||||
var rect = new Rect(
|
||||
Mathf.Min(start.x, end.x),
|
||||
Mathf.Min(start.y, end.y),
|
||||
Mathf.Abs(end.x - start.x),
|
||||
Mathf.Abs(end.y - start.y)
|
||||
);
|
||||
|
||||
// Draw fill
|
||||
GUI.color = boxColor;
|
||||
GUI.DrawTexture(rect, _fillTexture);
|
||||
|
||||
// Draw border
|
||||
GUI.color = borderColor;
|
||||
float b = 2f;
|
||||
GUI.DrawTexture(new Rect(rect.x, rect.y, rect.width, b), _borderTexture);
|
||||
GUI.DrawTexture(new Rect(rect.x, rect.yMax - b, rect.width, b), _borderTexture);
|
||||
GUI.DrawTexture(new Rect(rect.x, rect.y, b, rect.height), _borderTexture);
|
||||
GUI.DrawTexture(new Rect(rect.xMax - b, rect.y, b, rect.height), _borderTexture);
|
||||
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_fillTexture != null) Destroy(_fillTexture);
|
||||
if (_borderTexture != null) Destroy(_borderTexture);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Hybrid/SelectionBoxUI.cs.meta
Normal file
2
Assets/Scripts/Hybrid/SelectionBoxUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b197ee7ddd817294f9cd10d38d44efb9
|
||||
243
Assets/Scripts/Hybrid/SelectionManager.cs
Normal file
243
Assets/Scripts/Hybrid/SelectionManager.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Physics;
|
||||
using Unity.Transforms;
|
||||
using UnityEngine;
|
||||
using EE2Clone.Components;
|
||||
|
||||
namespace EE2Clone.Hybrid
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles click-select, shift-add, and box-drag selection of entities.
|
||||
/// Client-side only. Adds/removes SelectedTag on ECS entities.
|
||||
/// </summary>
|
||||
public class SelectionManager : MonoBehaviour
|
||||
{
|
||||
public static SelectionManager Instance { get; private set; }
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private LayerMask selectableLayers = ~0;
|
||||
[SerializeField] private float dragThreshold = 5f;
|
||||
|
||||
public List<Entity> SelectedEntities { get; } = new();
|
||||
|
||||
// Control groups: index 0-9
|
||||
private readonly List<Entity>[] _controlGroups = new List<Entity>[10];
|
||||
|
||||
// Box selection state
|
||||
private bool _isDragging;
|
||||
private Vector2 _dragStart;
|
||||
|
||||
// Exposed for SelectionBoxUI
|
||||
public bool IsDragging => _isDragging;
|
||||
public Vector2 DragStart => _dragStart;
|
||||
public Vector2 DragEnd { get; private set; }
|
||||
|
||||
private Camera _mainCamera;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
||||
Instance = this;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
_controlGroups[i] = new List<Entity>();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_mainCamera = Camera.main;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var input = RTSInputActions.Instance;
|
||||
if (input == null || _mainCamera == null) return;
|
||||
|
||||
HandleSelection(input);
|
||||
HandleControlGroups(input);
|
||||
}
|
||||
|
||||
private void HandleSelection(RTSInputActions input)
|
||||
{
|
||||
if (input.LeftClickPressed)
|
||||
{
|
||||
_dragStart = input.MousePosition;
|
||||
_isDragging = false;
|
||||
}
|
||||
|
||||
if (input.LeftClickHeld)
|
||||
{
|
||||
DragEnd = input.MousePosition;
|
||||
if (Vector2.Distance(_dragStart, DragEnd) > dragThreshold)
|
||||
_isDragging = true;
|
||||
}
|
||||
|
||||
if (input.LeftClickReleased)
|
||||
{
|
||||
DragEnd = input.MousePosition;
|
||||
|
||||
if (_isDragging)
|
||||
BoxSelect(input.ShiftHeld);
|
||||
else
|
||||
ClickSelect(input.MousePosition, input.ShiftHeld);
|
||||
|
||||
_isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClickSelect(Vector2 screenPos, bool additive)
|
||||
{
|
||||
if (!additive)
|
||||
ClearSelection();
|
||||
|
||||
var ray = _mainCamera.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0));
|
||||
|
||||
// Raycast using Unity Physics (ECS)
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null || !world.IsCreated) return;
|
||||
|
||||
var physicsWorld = world.EntityManager
|
||||
.CreateEntityQuery(typeof(PhysicsWorldSingleton))
|
||||
.GetSingleton<PhysicsWorldSingleton>();
|
||||
|
||||
var rayInput = new RaycastInput
|
||||
{
|
||||
Start = ray.origin,
|
||||
End = ray.origin + ray.direction * 500f,
|
||||
Filter = new CollisionFilter
|
||||
{
|
||||
BelongsTo = ~0u,
|
||||
CollidesWith = ~0u,
|
||||
GroupIndex = 0
|
||||
}
|
||||
};
|
||||
|
||||
if (physicsWorld.CastRay(rayInput, out var hit))
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
var entity = physicsWorld.Bodies[hit.RigidBodyIndex].Entity;
|
||||
|
||||
if (em.HasComponent<UnitTag>(entity) || em.HasComponent<BuildingTag>(entity))
|
||||
{
|
||||
if (additive && SelectedEntities.Contains(entity))
|
||||
{
|
||||
SelectedEntities.Remove(entity);
|
||||
em.RemoveComponent<SelectedTag>(entity);
|
||||
}
|
||||
else if (!SelectedEntities.Contains(entity))
|
||||
{
|
||||
SelectedEntities.Add(entity);
|
||||
em.AddComponent<SelectedTag>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BoxSelect(bool additive)
|
||||
{
|
||||
if (!additive)
|
||||
ClearSelection();
|
||||
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null || !world.IsCreated) return;
|
||||
|
||||
var em = world.EntityManager;
|
||||
|
||||
var min = Vector2.Min(_dragStart, DragEnd);
|
||||
var max = Vector2.Max(_dragStart, DragEnd);
|
||||
|
||||
// Query all unit entities and check if their screen position is within the box
|
||||
var query = em.CreateEntityQuery(typeof(UnitTag), typeof(LocalTransform));
|
||||
var entities = query.ToEntityArray(Unity.Collections.Allocator.Temp);
|
||||
var transforms = query.ToComponentDataArray<LocalTransform>(Unity.Collections.Allocator.Temp);
|
||||
|
||||
for (int i = 0; i < entities.Length; i++)
|
||||
{
|
||||
var screenPos = _mainCamera.WorldToScreenPoint(transforms[i].Position);
|
||||
if (screenPos.z > 0 &&
|
||||
screenPos.x >= min.x && screenPos.x <= max.x &&
|
||||
screenPos.y >= min.y && screenPos.y <= max.y)
|
||||
{
|
||||
var entity = entities[i];
|
||||
if (!SelectedEntities.Contains(entity))
|
||||
{
|
||||
SelectedEntities.Add(entity);
|
||||
if (!em.HasComponent<SelectedTag>(entity))
|
||||
em.AddComponent<SelectedTag>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entities.Dispose();
|
||||
transforms.Dispose();
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world != null && world.IsCreated)
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
foreach (var entity in SelectedEntities)
|
||||
{
|
||||
if (em.Exists(entity) && em.HasComponent<SelectedTag>(entity))
|
||||
em.RemoveComponent<SelectedTag>(entity);
|
||||
}
|
||||
}
|
||||
SelectedEntities.Clear();
|
||||
}
|
||||
|
||||
private void HandleControlGroups(RTSInputActions input)
|
||||
{
|
||||
int group = input.ControlGroupPressed;
|
||||
if (group < 0) return;
|
||||
|
||||
if (input.CtrlHeld)
|
||||
{
|
||||
// Assign current selection to control group
|
||||
_controlGroups[group].Clear();
|
||||
_controlGroups[group].AddRange(SelectedEntities);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Recall control group
|
||||
ClearSelection();
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null || !world.IsCreated) return;
|
||||
var em = world.EntityManager;
|
||||
|
||||
foreach (var entity in _controlGroups[group])
|
||||
{
|
||||
if (em.Exists(entity))
|
||||
{
|
||||
SelectedEntities.Add(entity);
|
||||
if (!em.HasComponent<SelectedTag>(entity))
|
||||
em.AddComponent<SelectedTag>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Entity[] GetSelectedEntitiesOfType<T>() where T : unmanaged, IComponentData
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null || !world.IsCreated) return System.Array.Empty<Entity>();
|
||||
|
||||
var em = world.EntityManager;
|
||||
var result = new List<Entity>();
|
||||
foreach (var entity in SelectedEntities)
|
||||
{
|
||||
if (em.Exists(entity) && em.HasComponent<T>(entity))
|
||||
result.Add(entity);
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Hybrid/SelectionManager.cs.meta
Normal file
2
Assets/Scripts/Hybrid/SelectionManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 666bdebfedde0ad42b1fcc77e0e27843
|
||||
8
Assets/Scripts/NetCode.meta
Normal file
8
Assets/Scripts/NetCode.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3e8c2c9ff1dcf374ca5d87a59403b894
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
32
Assets/Scripts/NetCode/ConnectionMonitorSystem.cs
Normal file
32
Assets/Scripts/NetCode/ConnectionMonitorSystem.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace EE2Clone.NetCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-side system that monitors for disconnected players and logs events.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct ConnectionMonitorSystem : ISystem
|
||||
{
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkId>();
|
||||
}
|
||||
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
// Count active connections for logging/debugging
|
||||
int connectionCount = 0;
|
||||
foreach (var _ in SystemAPI.Query<RefRO<NetworkId>>()
|
||||
.WithAll<NetworkStreamInGame>())
|
||||
{
|
||||
connectionCount++;
|
||||
}
|
||||
|
||||
// Future: detect disconnections and handle AI takeover
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/NetCode/ConnectionMonitorSystem.cs.meta
Normal file
2
Assets/Scripts/NetCode/ConnectionMonitorSystem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb810a5f20d826844a1063b2a900fa0a
|
||||
23
Assets/Scripts/NetCode/GameBootstrap.cs
Normal file
23
Assets/Scripts/NetCode/GameBootstrap.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace EE2Clone.NetCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom bootstrap that prevents automatic world creation.
|
||||
/// We manually create client/server worlds when the player chooses to host or connect.
|
||||
/// </summary>
|
||||
[UnityEngine.Scripting.Preserve]
|
||||
public class GameBootstrap : ClientServerBootstrap
|
||||
{
|
||||
public override bool Initialize(string defaultWorldName)
|
||||
{
|
||||
// Create only the default (local) world on startup.
|
||||
// Client and server worlds are created manually when the user
|
||||
// starts hosting or joins a game.
|
||||
AutoConnectPort = 0;
|
||||
CreateDefaultClientServerWorlds();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/NetCode/GameBootstrap.cs.meta
Normal file
2
Assets/Scripts/NetCode/GameBootstrap.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 278d5b5c203631c42a5f1eb814815e6c
|
||||
52
Assets/Scripts/NetCode/GoInGameClientSystem.cs
Normal file
52
Assets/Scripts/NetCode/GoInGameClientSystem.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace EE2Clone.NetCode
|
||||
{
|
||||
/// <summary>
|
||||
/// RPC sent by the client to request going in-game.
|
||||
/// </summary>
|
||||
public struct GoInGameRequest : IRpcCommand
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client-side system: when a connection is established and the streaming is done,
|
||||
/// send a GoInGameRequest RPC to the server.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
|
||||
public partial struct GoInGameClientSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkId>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (networkId, entity) in
|
||||
SystemAPI.Query<RefRO<NetworkId>>()
|
||||
.WithNone<NetworkStreamInGame>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
ecb.AddComponent<NetworkStreamInGame>(entity);
|
||||
|
||||
var requestEntity = ecb.CreateEntity();
|
||||
ecb.AddComponent(requestEntity, new GoInGameRequest());
|
||||
ecb.AddComponent(requestEntity, new SendRpcCommandRequest { TargetConnection = entity });
|
||||
|
||||
UnityEngine.Debug.Log($"[Client] Sending GoInGame request (NetworkId={networkId.ValueRO.Value})");
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/NetCode/GoInGameClientSystem.cs.meta
Normal file
2
Assets/Scripts/NetCode/GoInGameClientSystem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2bae91c6664cbd4796e97de29d6b1a7
|
||||
77
Assets/Scripts/NetCode/GoInGameServerSystem.cs
Normal file
77
Assets/Scripts/NetCode/GoInGameServerSystem.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using EE2Clone.Components;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.NetCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-side system: processes GoInGameRequest RPCs, marks connections as in-game,
|
||||
/// and creates a PlayerState entity for each connecting player.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct GoInGameServerSystem : ISystem
|
||||
{
|
||||
private int _nextPlayerId;
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<GoInGameRequest>();
|
||||
_nextPlayerId = 1;
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (request, requestSource, requestEntity) in
|
||||
SystemAPI.Query<RefRO<GoInGameRequest>, RefRO<ReceiveRpcCommandRequest>>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
var connectionEntity = requestSource.ValueRO.SourceConnection;
|
||||
|
||||
// Mark the connection as in-game
|
||||
ecb.AddComponent<NetworkStreamInGame>(connectionEntity);
|
||||
|
||||
// Get the NetworkId for this connection
|
||||
var networkId = state.EntityManager.GetComponentData<NetworkId>(connectionEntity);
|
||||
|
||||
// Create a PlayerState entity for this player
|
||||
var playerEntity = ecb.CreateEntity();
|
||||
ecb.AddComponent(playerEntity, new PlayerStateComponent
|
||||
{
|
||||
PlayerId = _nextPlayerId,
|
||||
CurrentEpoch = Epoch.StoneAge,
|
||||
PopulationCurrent = 0,
|
||||
PopulationMax = GameConstants.StartingPopulationCap,
|
||||
CivilizationId = 0,
|
||||
IsAlive = true
|
||||
});
|
||||
ecb.AddComponent(playerEntity, new PlayerResourcesComponent
|
||||
{
|
||||
Food = 200,
|
||||
Wood = 200,
|
||||
Stone = 100,
|
||||
Gold = 100,
|
||||
Tin = 0
|
||||
});
|
||||
ecb.AddComponent(playerEntity, new GhostOwner { NetworkId = networkId.Value });
|
||||
|
||||
UnityEngine.Debug.Log($"[Server] Player {_nextPlayerId} (NetworkId={networkId.Value}) entered game");
|
||||
|
||||
_nextPlayerId++;
|
||||
|
||||
// Destroy the request entity
|
||||
ecb.DestroyEntity(requestEntity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/NetCode/GoInGameServerSystem.cs.meta
Normal file
2
Assets/Scripts/NetCode/GoInGameServerSystem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c5c4cafaa538c64da4f890ef8658f40
|
||||
95
Assets/Scripts/NetCode/RpcCommands.cs
Normal file
95
Assets/Scripts/NetCode/RpcCommands.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.NetCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Client requests to move selected units to a position.
|
||||
/// </summary>
|
||||
public struct MoveCommandRpc : IRpcCommand
|
||||
{
|
||||
public Entity UnitEntity;
|
||||
public float3 TargetPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests to attack a target with selected units.
|
||||
/// </summary>
|
||||
public struct AttackCommandRpc : IRpcCommand
|
||||
{
|
||||
public Entity AttackerEntity;
|
||||
public Entity TargetEntity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests to place a building.
|
||||
/// </summary>
|
||||
public struct PlaceBuildingRpc : IRpcCommand
|
||||
{
|
||||
public BuildingType Type;
|
||||
public float3 Position;
|
||||
public quaternion Rotation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests to set a rally point on a building.
|
||||
/// </summary>
|
||||
public struct SetRallyPointRpc : IRpcCommand
|
||||
{
|
||||
public Entity BuildingEntity;
|
||||
public float3 Position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests to queue unit production in a building.
|
||||
/// </summary>
|
||||
public struct QueueUnitProductionRpc : IRpcCommand
|
||||
{
|
||||
public Entity BuildingEntity;
|
||||
public int UnitDataId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests to gather from a resource node.
|
||||
/// </summary>
|
||||
public struct GatherCommandRpc : IRpcCommand
|
||||
{
|
||||
public Entity CitizenEntity;
|
||||
public Entity ResourceNodeEntity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests to assign citizens to build/repair a building.
|
||||
/// </summary>
|
||||
public struct BuildRepairCommandRpc : IRpcCommand
|
||||
{
|
||||
public Entity CitizenEntity;
|
||||
public Entity BuildingEntity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests to research a technology.
|
||||
/// </summary>
|
||||
public struct ResearchTechRpc : IRpcCommand
|
||||
{
|
||||
public Entity BuildingEntity;
|
||||
public int TechId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests to advance to the next epoch.
|
||||
/// </summary>
|
||||
public struct EpochAdvanceRpc : IRpcCommand
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests to stop selected units.
|
||||
/// </summary>
|
||||
public struct StopCommandRpc : IRpcCommand
|
||||
{
|
||||
public Entity UnitEntity;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/NetCode/RpcCommands.cs.meta
Normal file
2
Assets/Scripts/NetCode/RpcCommands.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62f6a545bb8db214a97318bcba211f01
|
||||
8
Assets/Scripts/Systems.meta
Normal file
8
Assets/Scripts/Systems.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53d2d11d9c714444cb6bc94d3154cb00
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
197
Assets/Scripts/Systems/BuildingSystems.cs
Normal file
197
Assets/Scripts/Systems/BuildingSystems.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
using Unity.NetCode;
|
||||
using EE2Clone.Components;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-side: processes PlaceBuildingRpc — validates and spawns building entities.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct PlaceBuildingCommandSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (cmd, source, entity) in
|
||||
SystemAPI.Query<RefRO<PlaceBuildingRpc>, RefRO<ReceiveRpcCommandRequest>>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
// TODO: Validate resources, territory, collision
|
||||
// TODO: Lookup building prefab entity from BuildingType
|
||||
// TODO: Instantiate the prefab and place it
|
||||
|
||||
// For now just log
|
||||
UnityEngine.Debug.Log($"[Server] PlaceBuilding request: {cmd.ValueRO.Type} at {cmd.ValueRO.Position}");
|
||||
|
||||
ecb.DestroyEntity(entity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side: processes BuildRepairCommandRpc — assigns citizens to build/repair buildings.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct BuildRepairCommandSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (cmd, source, entity) in
|
||||
SystemAPI.Query<RefRO<BuildRepairCommandRpc>, RefRO<ReceiveRpcCommandRequest>>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
var citizen = cmd.ValueRO.CitizenEntity;
|
||||
var building = cmd.ValueRO.BuildingEntity;
|
||||
|
||||
if (state.EntityManager.Exists(citizen) && state.EntityManager.Exists(building) &&
|
||||
state.EntityManager.HasComponent<CitizenStateComponent>(citizen))
|
||||
{
|
||||
state.EntityManager.SetComponentData(citizen, new BuildTarget { Target = building });
|
||||
state.EntityManager.SetComponentData(citizen, new CitizenStateComponent { Value = CitizenState.MovingToBuild });
|
||||
|
||||
var buildingPos = state.EntityManager.GetComponentData<LocalTransform>(building).Position;
|
||||
state.EntityManager.SetComponentData(citizen, new MoveTarget
|
||||
{
|
||||
Position = buildingPos,
|
||||
IsActive = true
|
||||
});
|
||||
}
|
||||
|
||||
ecb.DestroyEntity(entity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side: ticks construction progress for buildings with assigned citizens.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateAfter(typeof(BuildRepairCommandSystem))]
|
||||
public partial struct ConstructionSystem : ISystem
|
||||
{
|
||||
private float _timer;
|
||||
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
_timer = 0f;
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
_timer += dt;
|
||||
if (_timer < GameConstants.ConstructionTickInterval) return;
|
||||
_timer -= GameConstants.ConstructionTickInterval;
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
// Count builders per building
|
||||
// Simple approach: iterate citizens in Building state
|
||||
foreach (var (citizenState, buildTarget, transform, moveTarget) in
|
||||
SystemAPI.Query<RefRW<CitizenStateComponent>, RefRO<BuildTarget>,
|
||||
RefRO<LocalTransform>, RefRO<MoveTarget>>()
|
||||
.WithAll<CitizenTag>())
|
||||
{
|
||||
if (citizenState.ValueRO.Value == CitizenState.MovingToBuild && !moveTarget.ValueRO.IsActive)
|
||||
{
|
||||
citizenState.ValueRW.Value = CitizenState.Building;
|
||||
}
|
||||
|
||||
if (citizenState.ValueRO.Value != CitizenState.Building) continue;
|
||||
|
||||
var buildingEntity = buildTarget.ValueRO.Target;
|
||||
if (buildingEntity == Entity.Null || !state.EntityManager.Exists(buildingEntity)) continue;
|
||||
|
||||
if (!state.EntityManager.HasComponent<ConstructionProgress>(buildingEntity)) continue;
|
||||
|
||||
var progress = state.EntityManager.GetComponentData<ConstructionProgress>(buildingEntity);
|
||||
float increment = GameConstants.ConstructionTickInterval / progress.BuildTime;
|
||||
progress.Progress = math.min(progress.Progress + increment, 1f);
|
||||
state.EntityManager.SetComponentData(buildingEntity, progress);
|
||||
|
||||
// Complete construction
|
||||
if (progress.Progress >= 1f)
|
||||
{
|
||||
ecb.RemoveComponent<UnderConstructionTag>(buildingEntity);
|
||||
ecb.RemoveComponent<ConstructionProgress>(buildingEntity);
|
||||
|
||||
// Set health to max
|
||||
var health = state.EntityManager.GetComponentData<Health>(buildingEntity);
|
||||
health.Current = health.Max;
|
||||
ecb.SetComponent(buildingEntity, health);
|
||||
|
||||
// Citizen becomes idle
|
||||
citizenState.ValueRW.Value = CitizenState.Idle;
|
||||
}
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side: processes production queues on buildings and spawns units.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct ProductionSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (productionQueue, rallyPoint, owner, transform, entity) in
|
||||
SystemAPI.Query<DynamicBuffer<ProductionQueueElement>, RefRO<RallyPoint>,
|
||||
RefRO<OwnerPlayer>, RefRO<LocalTransform>>()
|
||||
.WithAll<BuildingTag>()
|
||||
.WithNone<UnderConstructionTag>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
if (productionQueue.Length == 0) continue;
|
||||
|
||||
var front = productionQueue[0];
|
||||
front.TimeRemaining -= dt;
|
||||
|
||||
if (front.TimeRemaining <= 0)
|
||||
{
|
||||
// TODO: Instantiate unit prefab based on front.UnitDataId
|
||||
// For now just log
|
||||
UnityEngine.Debug.Log($"[Server] Unit produced: DataId={front.UnitDataId} at {rallyPoint.ValueRO.Position}");
|
||||
|
||||
productionQueue.RemoveAt(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
productionQueue[0] = front;
|
||||
}
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Systems/BuildingSystems.cs.meta
Normal file
2
Assets/Scripts/Systems/BuildingSystems.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4a7a8d853e94f941b762acafa1de53d
|
||||
207
Assets/Scripts/Systems/CombatSystems.cs
Normal file
207
Assets/Scripts/Systems/CombatSystems.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
using Unity.NetCode;
|
||||
using EE2Clone.Components;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-side: processes AttackCommandRpc — sets combat target on units.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct AttackCommandSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (cmd, source, entity) in
|
||||
SystemAPI.Query<RefRO<AttackCommandRpc>, RefRO<ReceiveRpcCommandRequest>>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
var attacker = cmd.ValueRO.AttackerEntity;
|
||||
var target = cmd.ValueRO.TargetEntity;
|
||||
|
||||
if (state.EntityManager.Exists(attacker) && state.EntityManager.Exists(target) &&
|
||||
state.EntityManager.HasComponent<CombatTarget>(attacker))
|
||||
{
|
||||
state.EntityManager.SetComponentData(attacker, new CombatTarget { Target = target });
|
||||
state.EntityManager.SetComponentData(attacker, new UnitStateComponent { Value = UnitState.Attacking });
|
||||
}
|
||||
|
||||
ecb.DestroyEntity(entity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side: units with a CombatTarget move toward target, then deal damage when in range.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateAfter(typeof(AttackCommandSystem))]
|
||||
public partial struct CombatSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
|
||||
foreach (var (transform, attack, combatTarget, speed, unitState, unitClass, entity) in
|
||||
SystemAPI.Query<RefRW<LocalTransform>, RefRW<AttackData>, RefRW<CombatTarget>,
|
||||
RefRO<MovementSpeed>, RefRW<UnitStateComponent>, RefRO<UnitClassComponent>>()
|
||||
.WithAll<UnitTag>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
// Tick cooldown
|
||||
if (attack.ValueRO.CooldownRemaining > 0)
|
||||
attack.ValueRW.CooldownRemaining -= dt;
|
||||
|
||||
var target = combatTarget.ValueRO.Target;
|
||||
if (target == Entity.Null || !state.EntityManager.Exists(target))
|
||||
{
|
||||
// Clear invalid target
|
||||
if (target != Entity.Null)
|
||||
{
|
||||
combatTarget.ValueRW.Target = Entity.Null;
|
||||
if (unitState.ValueRO.Value == UnitState.Attacking)
|
||||
unitState.ValueRW.Value = UnitState.Idle;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!state.EntityManager.HasComponent<Health>(target))
|
||||
{
|
||||
combatTarget.ValueRW.Target = Entity.Null;
|
||||
unitState.ValueRW.Value = UnitState.Idle;
|
||||
continue;
|
||||
}
|
||||
|
||||
var targetTransform = state.EntityManager.GetComponentData<LocalTransform>(target);
|
||||
float3 toTarget = targetTransform.Position - transform.ValueRO.Position;
|
||||
toTarget.y = 0;
|
||||
float distance = math.length(toTarget);
|
||||
|
||||
if (distance > attack.ValueRO.Range)
|
||||
{
|
||||
// Move toward target
|
||||
float3 moveDir = math.normalize(toTarget);
|
||||
float moveAmount = math.min(speed.ValueRO.Value * dt, distance - attack.ValueRO.Range * 0.9f);
|
||||
transform.ValueRW.Position += moveDir * moveAmount;
|
||||
transform.ValueRW.Rotation = quaternion.LookRotationSafe(moveDir, math.up());
|
||||
}
|
||||
else if (attack.ValueRO.CooldownRemaining <= 0)
|
||||
{
|
||||
// Attack
|
||||
float damage = attack.ValueRO.Damage;
|
||||
|
||||
// Apply counter bonus
|
||||
if (state.EntityManager.HasComponent<UnitClassComponent>(target))
|
||||
{
|
||||
var targetClass = state.EntityManager.GetComponentData<UnitClassComponent>(target).Value;
|
||||
float counterMult = GetCounterMultiplier(unitClass.ValueRO.Value, targetClass);
|
||||
damage *= counterMult;
|
||||
}
|
||||
|
||||
// Apply armor
|
||||
if (state.EntityManager.HasComponent<ArmorData>(target))
|
||||
{
|
||||
float armor = state.EntityManager.GetComponentData<ArmorData>(target).Value;
|
||||
damage = math.max(damage - armor, GameConstants.MinDamage);
|
||||
}
|
||||
|
||||
var targetHealth = state.EntityManager.GetComponentData<Health>(target);
|
||||
targetHealth.Current -= damage;
|
||||
state.EntityManager.SetComponentData(target, targetHealth);
|
||||
|
||||
attack.ValueRW.CooldownRemaining = attack.ValueRO.AttackCooldown;
|
||||
|
||||
// Face target
|
||||
if (distance > 0.01f)
|
||||
transform.ValueRW.Rotation = quaternion.LookRotationSafe(math.normalize(toTarget), math.up());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float GetCounterMultiplier(UnitClass attacker, UnitClass target)
|
||||
{
|
||||
// Rock-paper-scissors: Infantry > Ranged > Cavalry > Infantry
|
||||
if (attacker == UnitClass.Infantry && target == UnitClass.Ranged) return GameConstants.CounterBonusStrong;
|
||||
if (attacker == UnitClass.Ranged && target == UnitClass.Cavalry) return GameConstants.CounterBonusStrong;
|
||||
if (attacker == UnitClass.Cavalry && target == UnitClass.Infantry) return GameConstants.CounterBonusStrong;
|
||||
|
||||
if (attacker == UnitClass.Infantry && target == UnitClass.Cavalry) return GameConstants.CounterBonusWeak;
|
||||
if (attacker == UnitClass.Ranged && target == UnitClass.Infantry) return GameConstants.CounterBonusWeak;
|
||||
if (attacker == UnitClass.Cavalry && target == UnitClass.Ranged) return GameConstants.CounterBonusWeak;
|
||||
|
||||
// Siege bonus vs buildings (handled via BuildingTag check in caller if needed)
|
||||
return GameConstants.CounterBonusNeutral;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side: destroys entities that have 0 or less HP after a brief delay.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateAfter(typeof(CombatSystem))]
|
||||
public partial struct DeathSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
|
||||
// Tag entities with <=0 HP for destruction
|
||||
foreach (var (health, entity) in
|
||||
SystemAPI.Query<RefRO<Health>>()
|
||||
.WithNone<DeathTimer, DestroyEntityTag>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
if (health.ValueRO.Current <= 0)
|
||||
{
|
||||
ecb.AddComponent(entity, new DeathTimer { TimeRemaining = 1.0f });
|
||||
|
||||
if (state.EntityManager.HasComponent<UnitStateComponent>(entity))
|
||||
{
|
||||
ecb.SetComponent(entity, new UnitStateComponent { Value = UnitState.Dying });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tick death timers
|
||||
foreach (var (deathTimer, entity) in
|
||||
SystemAPI.Query<RefRW<DeathTimer>>()
|
||||
.WithNone<DestroyEntityTag>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
deathTimer.ValueRW.TimeRemaining -= dt;
|
||||
if (deathTimer.ValueRO.TimeRemaining <= 0)
|
||||
{
|
||||
ecb.AddComponent<DestroyEntityTag>(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy tagged entities
|
||||
foreach (var (tag, entity) in
|
||||
SystemAPI.Query<RefRO<DestroyEntityTag>>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
ecb.DestroyEntity(entity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Systems/CombatSystems.cs.meta
Normal file
2
Assets/Scripts/Systems/CombatSystems.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1163cc3a2970481429c3bd67549beb78
|
||||
172
Assets/Scripts/Systems/GatheringSystems.cs
Normal file
172
Assets/Scripts/Systems/GatheringSystems.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
using Unity.NetCode;
|
||||
using EE2Clone.Components;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-side: processes GatherCommandRpc — assigns a gather target to citizens.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct GatherCommandSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (cmd, source, entity) in
|
||||
SystemAPI.Query<RefRO<GatherCommandRpc>, RefRO<ReceiveRpcCommandRequest>>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
var citizen = cmd.ValueRO.CitizenEntity;
|
||||
var node = cmd.ValueRO.ResourceNodeEntity;
|
||||
|
||||
if (state.EntityManager.Exists(citizen) && state.EntityManager.Exists(node) &&
|
||||
state.EntityManager.HasComponent<CitizenStateComponent>(citizen) &&
|
||||
state.EntityManager.HasComponent<ResourceNode>(node))
|
||||
{
|
||||
state.EntityManager.SetComponentData(citizen, new GatherTarget { Target = node });
|
||||
state.EntityManager.SetComponentData(citizen, new CitizenStateComponent { Value = CitizenState.MovingToGather });
|
||||
|
||||
// Set move target to the resource node's position
|
||||
var nodePos = state.EntityManager.GetComponentData<LocalTransform>(node).Position;
|
||||
state.EntityManager.SetComponentData(citizen, new MoveTarget
|
||||
{
|
||||
Position = nodePos,
|
||||
IsActive = true
|
||||
});
|
||||
}
|
||||
|
||||
ecb.DestroyEntity(entity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side: citizen gathering state machine.
|
||||
/// Handles: MovingToGather → Gathering → MovingToDropoff → Depositing → repeat.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateAfter(typeof(GatherCommandSystem))]
|
||||
public partial struct GatheringSystem : ISystem
|
||||
{
|
||||
private float _gatherTimer;
|
||||
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
_gatherTimer = 0f;
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
_gatherTimer += dt;
|
||||
bool gatherTick = _gatherTimer >= GameConstants.GatherTickInterval;
|
||||
if (gatherTick) _gatherTimer -= GameConstants.GatherTickInterval;
|
||||
|
||||
foreach (var (citizenState, gatherTarget, carried, transform, moveTarget, owner) in
|
||||
SystemAPI.Query<RefRW<CitizenStateComponent>, RefRW<GatherTarget>,
|
||||
RefRW<CarriedResource>, RefRO<LocalTransform>, RefRW<MoveTarget>, RefRO<OwnerPlayer>>()
|
||||
.WithAll<CitizenTag>())
|
||||
{
|
||||
switch (citizenState.ValueRO.Value)
|
||||
{
|
||||
case CitizenState.MovingToGather:
|
||||
// Check if arrived (MoveTarget will be deactivated by movement system)
|
||||
if (!moveTarget.ValueRO.IsActive)
|
||||
{
|
||||
citizenState.ValueRW.Value = CitizenState.Gathering;
|
||||
}
|
||||
break;
|
||||
|
||||
case CitizenState.Gathering:
|
||||
if (!gatherTick) break;
|
||||
|
||||
var targetEntity = gatherTarget.ValueRO.Target;
|
||||
if (targetEntity == Entity.Null || !state.EntityManager.Exists(targetEntity))
|
||||
{
|
||||
citizenState.ValueRW.Value = CitizenState.Idle;
|
||||
break;
|
||||
}
|
||||
|
||||
var node = state.EntityManager.GetComponentData<ResourceNode>(targetEntity);
|
||||
if (node.RemainingAmount <= 0)
|
||||
{
|
||||
citizenState.ValueRW.Value = CitizenState.Idle;
|
||||
gatherTarget.ValueRW.Target = Entity.Null;
|
||||
break;
|
||||
}
|
||||
|
||||
// Gather one unit
|
||||
int gatherAmount = math.min(1, node.RemainingAmount);
|
||||
gatherAmount = math.min(gatherAmount, carried.ValueRO.MaxCarryCapacity - carried.ValueRO.Amount);
|
||||
|
||||
if (gatherAmount > 0)
|
||||
{
|
||||
node.RemainingAmount -= gatherAmount;
|
||||
state.EntityManager.SetComponentData(targetEntity, node);
|
||||
|
||||
carried.ValueRW.Amount += gatherAmount;
|
||||
carried.ValueRW.Type = node.Type;
|
||||
}
|
||||
|
||||
// If carrying capacity full, go to dropoff
|
||||
if (carried.ValueRO.Amount >= carried.ValueRO.MaxCarryCapacity)
|
||||
{
|
||||
citizenState.ValueRW.Value = CitizenState.MovingToDropoff;
|
||||
// TODO: Find nearest dropoff building and set as move target
|
||||
// For now, move back toward origin as placeholder
|
||||
moveTarget.ValueRW = new MoveTarget
|
||||
{
|
||||
Position = float3.zero,
|
||||
IsActive = true
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case CitizenState.MovingToDropoff:
|
||||
if (!moveTarget.ValueRO.IsActive)
|
||||
{
|
||||
citizenState.ValueRW.Value = CitizenState.Depositing;
|
||||
}
|
||||
break;
|
||||
|
||||
case CitizenState.Depositing:
|
||||
// Deposit resources into player economy
|
||||
// TODO: Find PlayerResourcesComponent for this owner and add resources
|
||||
carried.ValueRW.Amount = 0;
|
||||
|
||||
// Go back to gather if the node still exists
|
||||
if (gatherTarget.ValueRO.Target != Entity.Null &&
|
||||
state.EntityManager.Exists(gatherTarget.ValueRO.Target))
|
||||
{
|
||||
var nodePos = state.EntityManager.GetComponentData<LocalTransform>(gatherTarget.ValueRO.Target).Position;
|
||||
moveTarget.ValueRW = new MoveTarget
|
||||
{
|
||||
Position = nodePos,
|
||||
IsActive = true
|
||||
};
|
||||
citizenState.ValueRW.Value = CitizenState.MovingToGather;
|
||||
}
|
||||
else
|
||||
{
|
||||
citizenState.ValueRW.Value = CitizenState.Idle;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Systems/GatheringSystems.cs.meta
Normal file
2
Assets/Scripts/Systems/GatheringSystems.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35e43773511035f4bb29174417932b52
|
||||
151
Assets/Scripts/Systems/MovementSystems.cs
Normal file
151
Assets/Scripts/Systems/MovementSystems.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
using Unity.NetCode;
|
||||
using EE2Clone.Components;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-side system that processes MoveCommandRpc and sets MoveTarget on units.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct MoveCommandSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (cmd, source, entity) in
|
||||
SystemAPI.Query<RefRO<MoveCommandRpc>, RefRO<ReceiveRpcCommandRequest>>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
var unitEntity = cmd.ValueRO.UnitEntity;
|
||||
if (state.EntityManager.Exists(unitEntity) &&
|
||||
state.EntityManager.HasComponent<MoveTarget>(unitEntity))
|
||||
{
|
||||
state.EntityManager.SetComponentData(unitEntity, new MoveTarget
|
||||
{
|
||||
Position = cmd.ValueRO.TargetPosition,
|
||||
IsActive = true
|
||||
});
|
||||
|
||||
// Clear combat target when moving
|
||||
if (state.EntityManager.HasComponent<CombatTarget>(unitEntity))
|
||||
{
|
||||
state.EntityManager.SetComponentData(unitEntity, new CombatTarget
|
||||
{
|
||||
Target = Entity.Null
|
||||
});
|
||||
}
|
||||
|
||||
// Set state to Moving
|
||||
if (state.EntityManager.HasComponent<UnitStateComponent>(unitEntity))
|
||||
{
|
||||
state.EntityManager.SetComponentData(unitEntity, new UnitStateComponent
|
||||
{
|
||||
Value = UnitState.Moving
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ecb.DestroyEntity(entity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side system that moves units toward their MoveTarget.
|
||||
/// Placeholder for flow-field pathfinding — currently uses direct steering.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateAfter(typeof(MoveCommandSystem))]
|
||||
public partial struct UnitMovementSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
|
||||
foreach (var (transform, moveTarget, speed, unitState) in
|
||||
SystemAPI.Query<RefRW<LocalTransform>, RefRW<MoveTarget>, RefRO<MovementSpeed>, RefRW<UnitStateComponent>>()
|
||||
.WithAll<UnitTag>())
|
||||
{
|
||||
if (!moveTarget.ValueRO.IsActive) continue;
|
||||
|
||||
float3 pos = transform.ValueRO.Position;
|
||||
float3 target = moveTarget.ValueRO.Position;
|
||||
float3 direction = target - pos;
|
||||
direction.y = 0; // Keep on ground plane
|
||||
|
||||
float distance = math.length(direction);
|
||||
|
||||
if (distance < 0.5f)
|
||||
{
|
||||
// Arrived
|
||||
moveTarget.ValueRW.IsActive = false;
|
||||
if (unitState.ValueRO.Value == UnitState.Moving)
|
||||
unitState.ValueRW.Value = UnitState.Idle;
|
||||
}
|
||||
else
|
||||
{
|
||||
float3 moveDir = math.normalize(direction);
|
||||
float moveAmount = speed.ValueRO.Value * dt;
|
||||
moveAmount = math.min(moveAmount, distance);
|
||||
|
||||
var newPos = pos + moveDir * moveAmount;
|
||||
transform.ValueRW.Position = newPos;
|
||||
|
||||
// Face movement direction
|
||||
transform.ValueRW.Rotation = quaternion.LookRotationSafe(moveDir, math.up());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side: processes StopCommandRpc — halts unit movement and clears targets.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct StopCommandSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (cmd, source, entity) in
|
||||
SystemAPI.Query<RefRO<StopCommandRpc>, RefRO<ReceiveRpcCommandRequest>>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
var unitEntity = cmd.ValueRO.UnitEntity;
|
||||
if (state.EntityManager.Exists(unitEntity))
|
||||
{
|
||||
if (state.EntityManager.HasComponent<MoveTarget>(unitEntity))
|
||||
state.EntityManager.SetComponentData(unitEntity, new MoveTarget { IsActive = false });
|
||||
|
||||
if (state.EntityManager.HasComponent<CombatTarget>(unitEntity))
|
||||
state.EntityManager.SetComponentData(unitEntity, new CombatTarget { Target = Entity.Null });
|
||||
|
||||
if (state.EntityManager.HasComponent<UnitStateComponent>(unitEntity))
|
||||
state.EntityManager.SetComponentData(unitEntity, new UnitStateComponent { Value = UnitState.Idle });
|
||||
}
|
||||
|
||||
ecb.DestroyEntity(entity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Systems/MovementSystems.cs.meta
Normal file
2
Assets/Scripts/Systems/MovementSystems.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae6e692d140e8c24a9dd56224e814d43
|
||||
67
Assets/Scripts/Systems/PopulationSystem.cs
Normal file
67
Assets/Scripts/Systems/PopulationSystem.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using EE2Clone.Components;
|
||||
using EE2Clone.Core;
|
||||
|
||||
namespace EE2Clone.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-side: sums population-providing buildings per player and updates PopulationMax.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct PopulationSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
// Build a map of playerId → total pop capacity from buildings
|
||||
var popCapMap = new NativeHashMap<int, int>(GameConstants.MaxPlayers, Allocator.Temp);
|
||||
|
||||
foreach (var (pop, owner) in
|
||||
SystemAPI.Query<RefRO<ProvidesPopulation>, RefRO<OwnerPlayer>>()
|
||||
.WithAll<BuildingTag>()
|
||||
.WithNone<UnderConstructionTag>())
|
||||
{
|
||||
int playerId = owner.ValueRO.PlayerId;
|
||||
if (popCapMap.TryGetValue(playerId, out int current))
|
||||
popCapMap[playerId] = current + pop.ValueRO.Amount;
|
||||
else
|
||||
popCapMap[playerId] = GameConstants.StartingPopulationCap + pop.ValueRO.Amount;
|
||||
}
|
||||
|
||||
// Count current population per player (units)
|
||||
var popCurrentMap = new NativeHashMap<int, int>(GameConstants.MaxPlayers, Allocator.Temp);
|
||||
|
||||
foreach (var owner in SystemAPI.Query<RefRO<OwnerPlayer>>().WithAll<UnitTag>())
|
||||
{
|
||||
int playerId = owner.ValueRO.PlayerId;
|
||||
if (popCurrentMap.TryGetValue(playerId, out int current))
|
||||
popCurrentMap[playerId] = current + 1;
|
||||
else
|
||||
popCurrentMap[playerId] = 1;
|
||||
}
|
||||
|
||||
// Update PlayerStateComponent
|
||||
foreach (var playerState in SystemAPI.Query<RefRW<PlayerStateComponent>>())
|
||||
{
|
||||
int pid = playerState.ValueRO.PlayerId;
|
||||
|
||||
if (popCapMap.TryGetValue(pid, out int cap))
|
||||
playerState.ValueRW.PopulationMax = System.Math.Min(cap, GameConstants.MaxPopulationCap);
|
||||
else
|
||||
playerState.ValueRW.PopulationMax = GameConstants.StartingPopulationCap;
|
||||
|
||||
if (popCurrentMap.TryGetValue(pid, out int current))
|
||||
playerState.ValueRW.PopulationCurrent = current;
|
||||
else
|
||||
playerState.ValueRW.PopulationCurrent = 0;
|
||||
}
|
||||
|
||||
popCapMap.Dispose();
|
||||
popCurrentMap.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Systems/PopulationSystem.cs.meta
Normal file
2
Assets/Scripts/Systems/PopulationSystem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2ca22d7e5c3be04fb7f4d58ea50c36b
|
||||
Reference in New Issue
Block a user