Dự án ezytank-unity dùng cách tiếp cận lai (hybrid): ECS xử lý phần mô phỏng, GameObject xử lý phần hiển thị. Gói Unity.NetCode.Hybrid sẽ tự động đồng bộ hai phần này.

Mô phỏng và hiển thị

  • Mô phỏng (ECS): Entity, component (Tank, Shell, LocalToWorld) và system (TankMoveSystem, ShootSystem) quản lý logic và trạng thái, chạy ở cả server và client.
  • Hiển thị (GameObject): Các MonoBehaviour, MeshRenderer, ParticleSystem... dùng để hiển thị game cho người chơi, chỉ chạy ở client.

Khi entity thay đổi vị trí trong ECS, thành phần Transform của GameObject tương ứng cũng phải thay đổi theo.

Quy trình cài đặt:

  • Prefab trung tâm - GameResourcesAuthoring.cs: Script này giữ tham chiếu đến prefabs (ví dụ tankGhostPrefabs, shellGhost), và khi bake ECS, prefab được đưa vào component ECS để các system có thể truy cập.
    using Unity.Entities;
    using UnityEngine;
    
    namespace Youngmonkeys.EzyTank
    {
    	public class GameResourcesAuthoring : MonoBehaviour
    	{
    		[Header("Network Parameters")]
    		public uint despawnTicks = 30;
    
    		[Header("Ghost Prefabs")]
    		public GameObject[] tankGhostPrefabs;
    		public GameObject shellGhostPrefab;
    
    		public class Baker : Baker<GameResourcesAuthoring>
    		{
    			public override void Bake(GameResourcesAuthoring authoring)
    			{
    				Entity entity = GetEntity(TransformUsageFlags.None);
    				AddComponent(
    					entity,
    					new GameResources
    						{
    							despawnTicks = authoring.despawnTicks,
    							shellPrefabEntity = GetEntity(authoring.shellGhostPrefab, TransformUsageFlags.Dynamic),
    						}
    				);
    
    				// Add a buffer for multiple tank prefabs
    				DynamicBuffer<GameResourcesPlayerPrefab> buffer = AddBuffer<GameResourcesPlayerPrefab>(entity);
    				foreach (var prefab in authoring.tankGhostPrefabs)
    				{
    					if (prefab != null)
    					{
    						buffer.Add(new GameResourcesPlayerPrefab
    							{
    								playerPrefabEntity = GetEntity(prefab, TransformUsageFlags.Dynamic)
    							});
    					}
    				}
    
    			}
    		}
    	}
    
    	public struct GameResources : IComponentData
    	{
    		public uint despawnTicks;
    		public Entity shellPrefabEntity;
    	}
    
    	public struct GameResourcesPlayerPrefab : IBufferElementData
    	{
    		public Entity playerPrefabEntity;
    	}
    }
    
    
  • Ở Entity nào cần đồng bộ mô phỏng qua mạng (ghost) và cần hiển thị ở client, ta sẽ thêm component GhostPresentationGameObjectAuthoring, ví dụ ở prefab Tank như sau: rect864.png

    Ở đây prefab Tank dùng để tạo entity dùng cho việc đồng bộ mô phỏng giữa client và server, do đó nó chứa các components như TankAuthoringGhostAuthoringComponent. Ngược lại, prefab TankRenders chỉ gồm các game objects để hiện thị xe tăng ở máy người chơi, do đó chứa thành phần Transform, MeshFilterMeshRenderer. Đóng vai trò cầu nối là component GhostPresentationGameObjectAuthoring để tự động đồng bộ mô phỏng và hiển thị.

    Giải thích cơ chế khởi tạo và đồng bộ tự động:

    • GhostPresentationGameObjectSystem (client) phát hiện entity ghost chưa có GameObject → tự tạo prefab.
    • CopyTransformToGameObjectSystem mỗi frame sao chép LocalToWorld (vị trí, hướng) từ entity sang Transform của GameObject → Không cần viết code cập nhật thủ công.
  • Hủy và dọn dẹp: Khi entity bị xóa (ví dụ lúc tank nổ), DelayedDespawnSystem bỏ component tham chiếu prefab. Hệ thống sẽ hủy luôn GameObject tương ứng tránh để thừa trong scene.
    [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation | WorldSystemFilterFlags.ClientSimulation)]
    [UpdateInGroup(typeof(SimulationSystemGroup))]
    public partial struct DelayedDespawnSystem : ISystem
    {
    	...
    
    	public void OnUpdate(ref SystemState state)
    	{
    		DelayedDespawnJob job = new DelayedDespawnJob
    			{
    				...
    			};
    		state.Dependency = job.Schedule(state.Dependency);
    	}
    
    	public unsafe partial struct DelayedDespawnJob : IJobEntity
    	{
    		...
    
    		void Execute(Entity entity, ref DelayedDespawn delayedDespawn)
    		{
    
    			...
    
    
    			if (delayedDespawn.hasHandledPreDespawnClient == 0 && !isServer)
    			{
    				// Dispose tank renderer
    				if (gameObjectPrefabReferenceLookup.TryGetComponent(entity, out var gameObjectPrefabReference))
    				{
    					ecb.RemoveComponent<GhostPresentationGameObjectPrefabReference>(entity);
    				}
    				...
    			}
    		}
    	}
    }