这个是github上的一个游戏,我感觉他的代码结构设计得很好,所以看看。看每个Entitas工程感觉都是要从输入部分开始看,看输入的变量有哪些,然后看游戏的状态有哪些,这对了解整个游戏的过程有很多好处
一开始看个有点规模的Entitas还真是有点不习惯, 这个代码看引用不知道怎么去仔细找。
整体入口
和惯例的一样,也是通过一个monobehaviour脚本让Entitas与Unity的生命周期关联。在这个脚本里面将大System分了两块,一块是FixedUpdateSystems ,一块是UpdateSystems。 另外为了将引擎与框架能够较好分离开,需要引擎提供的功能都写到了Service里面,Service里面写了Unity的API提供需要的功能,在更换引擎的时候就只需要更换Service里面的内容就可以了。
using System.Collections.Generic;
using UnityEngine;
public class GameController : MonoBehaviour
{
private Contexts _contexts;
private FixedUpdateSystems _fixedUpdateSysems;
private Services _services;
private UpdateSystems _updateSystems;
[SerializeField]
private TextAsset _chunkDefinitions;
private void Awake()
{
_contexts = Contexts.sharedInstance;
_services = new Services();
Configure(_contexts);
CreateServices(_contexts, _services);
_updateSystems = new UpdateSystems(_contexts, _services);
_fixedUpdateSysems = new FixedUpdateSystems(_contexts, _services);
_updateSystems.Initialize();
_fixedUpdateSysems.Initialize();
}
private void Update()
{
_updateSystems.Execute();
_updateSystems.Cleanup();
}
private void FixedUpdate()
{
_fixedUpdateSysems.Execute();
_fixedUpdateSysems.Cleanup();
}
private void OnDestroy()
{
_updateSystems.DeactivateReactiveSystems();
_updateSystems.ClearReactiveSystems();
_updateSystems.TearDown();
_fixedUpdateSysems.DeactivateReactiveSystems();
_fixedUpdateSysems.ClearReactiveSystems();
_fixedUpdateSysems.TearDown();
}
//It's not in the system for ease of use
private void Configure(Contexts contexts)
{
//I would prefer to make it without any "EntityEmitters"..
//But it would be very time consuming OR hardly customizable
//One way is to look inside prefab for marker components (PlatformMonobehaviour, CoinMonobehaviour..)
//and parse those objects to JSON data for each chunk
//When chunk is prepooled - those objects with markers are removed. And when this chunk should be places in
//world - we pull it from pool AND pull interactible objects from their pools
//But it would take a day of work, so here I am :D
contexts.config.SetChunkDefinitions(JsonUtility.FromJson<ChunkDefinitions>(_chunkDefinitions.text));
contexts.config.SetChunkCreationDistance(200f);
contexts.config.SetChunkDestructionDistance(50f);
contexts.config.SetTypeCount(2);
contexts.config.SetTypeColorTable(new List<Color> {new Color(0.54f, 0.64f, 1f), new Color(1f, 0.6f, 0.54f), new Color(0.75f, 1f, 0.5f)});
contexts.config.SetJumpImpulse(new Vector2(0f, 18f));
contexts.config.SetJumpAcceleration(new Vector2(20f, -18f));
contexts.config.SetJumpMaxVelocity(new Vector2(20f, 45f));
contexts.config.SetMaxJumpCount(2);
contexts.config.SetMaxJumpTime(20f);
contexts.config.SetJumpEndOnStuckTimeout(0.1f);
contexts.config.SetStandardAcceleration(new Vector2(20f, -6f));
contexts.config.SetStandardMaxVelocity(new Vector2(20f, 70f));
contexts.config.SetAdditionalMidAirGravity(-35f);
contexts.config.SetVelocityIncreaseSpeed(80f);
contexts.config.SetVelocityDecreaseSpeed(2f);
contexts.config.SetStartPlayerPosition(new Vector2(0f, 4f));
contexts.config.SetPlayerSphereRadius(0.52f);
contexts.config.SetPlayerRespawnDelay(2f);
contexts.config.SetDeathHeight(-5f);
}
private void CreateServices(Contexts contexts, Services services)
{
services.IdService = new IdService(contexts);
services.ViewService = new UnityViewService(contexts);
services.InputService = new UnityInputService(contexts);
services.TimeService = new UnityTimeService(contexts);
services.PhysicsService = new UnityPhysicsService(contexts);
services.JumpService = new JumpService(contexts);
services.CreatePlayerService = new CreatePlayerService(contexts);
services.KillPlayerService = new KillPlayerService(contexts);
services.CreateChunkService = new CreateChunkService(contexts);
services.CollisionEmisstionService = new UnityCollisionEmissionService(contexts);
}
#if !ENTITAS_DISABLE_VISUAL_DEBUGGING
private void OnGUI()
{
var state = _contexts.gameState;
GUILayout.Label((state.isJumping ? "V" : "O") + " Jumping");
GUILayout.Label((state.isApplyJump ? "V" : "O") + " ApplyJump");
GUILayout.Label((state.isJumpImpulseFired ? "V" : "O") + " JumpImpulseFired");
GUILayout.Label((state.isJumpTimedOut ? "V" : "O") + " JumpTimedOut");
GUILayout.Label((state.isLanded ? "V" : "O") + " Landed");
GUILayout.Space(20f);
GUILayout.Label(state.lastJumpTime.Value + " LastJumpTime");
GUILayout.Label(state.currentJumpCount.Value + " JumpCount");
GUILayout.Label(state.currentMaxVelocity.Value + " MaxVelocity");
}
#endif
}
值得注意的是在OnDestroy()里面分别调用了主System的注销函数
_updateSystems.DeactivateReactiveSystems();
_updateSystems.ClearReactiveSystems();
_updateSystems.TearDown();
这是之前没有看过的,应该是比较正规的注销流程
主系统
public class FixedUpdateSystems : Feature
{
public FixedUpdateSystems(Contexts contexts, Services services)
{
Add(new GameSystems(contexts, services));
Add(new GameEventSystems(contexts));
Add(new DestroySystem(contexts));
}
}
public class UpdateSystems : Feature
{
public UpdateSystems(Contexts contexts, Services services)
{
Add(new ConfigEventSystems(contexts));
Add(new InputSystems(contexts, services));
Add(new GameStateSystems(contexts, services));
Add(new GameStateEventSystems(contexts));
}
}
Service
一堆的功能需要的Service
public class Services
{
public CreatePlayerService CreatePlayerService;
public CreateChunkService CreateChunkService;
public KillPlayerService KillPlayerService;
public IdService IdService;
public IInputService InputService;
public JumpService JumpService;
public ITimeService TimeService;
public IViewService ViewService;
public IPhysicsService PhysicsService;
public ICollisionEmisstionService CollisionEmisstionService;
}
private void CreateServices(Contexts contexts, Services services)
{
services.IdService = new IdService(contexts);
services.ViewService = new UnityViewService(contexts);
services.InputService = new UnityInputService(contexts);
services.TimeService = new UnityTimeService(contexts);
services.PhysicsService = new UnityPhysicsService(contexts);
services.JumpService = new JumpService(contexts);
services.CreatePlayerService = new CreatePlayerService(contexts);
services.KillPlayerService = new KillPlayerService(contexts);
services.CreateChunkService = new CreateChunkService(contexts);
services.CollisionEmisstionService = new UnityCollisionEmissionService(contexts);
}
在需要的System里面传入,由System持有对应的Service进行获取所需的数据或者进行所需的操作
如
public sealed class UpdateTimeSystem : IExecuteSystem, IInitializeSystem
{
private readonly Contexts _contexts;
private readonly ITimeService _timeService;
public UpdateTimeSystem(Contexts contexts, Services services)
{
_contexts = contexts;
_timeService = services.TimeService;
}
public void Initialize()
{
//Make it bulletproof
Execute();
}
public void Execute()
{
_contexts.input.ReplaceFixedDeltaTime(_timeService.FixedDeltaTime());
_contexts.input.ReplaceDeltaTime(_timeService.DeltaTime());
_contexts.input.ReplaceRealtimeSinceStartup(_timeService.RealtimeSinceStartup());
}
}
整体配置部分
查看Context.cs可以看见总共生成了四个Context上下文()
public ConfigContext config { get; set; }
public GameContext game { get; set; }
public GameStateContext gameState { get; set; }
public InputContext input { get; set; }
其中的配置部分Context,其下的组件全部都是被Unique标签定义了的组件,每个这样被Unique标签定义了的组件在Config这个上下文中只有一个带有这个组件的实体。实体都会对应生成属于Config这个Context的对象方法,SetXXX,ReplaceXX, RemoveXXX等,如下
public partial class ConfigContext {
public ConfigEntity jumpMaxVelocityEntity { get { return GetGroup(ConfigMatcher.JumpMaxVelocity).GetSingleEntity(); } }
public JumpMaxVelocityComponent jumpMaxVelocity { get { return jumpMaxVelocityEntity.jumpMaxVelocity; } }
public bool hasJumpMaxVelocity { get { return jumpMaxVelocityEntity != null; } }
public ConfigEntity SetJumpMaxVelocity(UnityEngine.Vector2 newValue) {
if (hasJumpMaxVelocity) {
throw new Entitas.EntitasException("Could not set JumpMaxVelocity!\n" + this + " already has an entity with JumpMaxVelocityComponent!",
"You should check if the context already has a jumpMaxVelocityEntity before setting it or use context.ReplaceJumpMaxVelocity().");
}
var entity = CreateEntity();
entity.AddJumpMaxVelocity(newValue);
return entity;
}
public void ReplaceJumpMaxVelocity(UnityEngine.Vector2 newValue) {
var entity = jumpMaxVelocityEntity;
if (entity == null) {
entity = SetJumpMaxVelocity(newValue);
} else {
entity.ReplaceJumpMaxVelocity(newValue);
}
}
public void RemoveJumpMaxVelocity() {
jumpMaxVelocityEntity.Destroy();
}
}
利用这个特性只对Config进行各种设置组件属性的操作,相当于用Entitas的方式将Config这个Context作为了一个静态的配置数据类,在一开始的时候进行配置数据的设置。
private void Configure(Contexts contexts)
{
//I would prefer to make it without any "EntityEmitters"..
//But it would be very time consuming OR hardly customizable
//One way is to look inside prefab for marker components (PlatformMonobehaviour, CoinMonobehaviour..)
//and parse those objects to JSON data for each chunk
//When chunk is prepooled - those objects with markers are removed. And when this chunk should be places in
//world - we pull it from pool AND pull interactible objects from their pools
//But it would take a day of work, so here I am :D
contexts.config.SetChunkDefinitions(JsonUtility.FromJson<ChunkDefinitions>(_chunkDefinitions.text));
contexts.config.SetChunkCreationDistance(200f);
contexts.config.SetChunkDestructionDistance(50f);
contexts.config.SetTypeCount(2);
contexts.config.SetTypeColorTable(new List<Color> {new Color(0.54f, 0.64f, 1f), new Color(1f, 0.6f, 0.54f), new Color(0.75f, 1f, 0.5f)});
contexts.config.SetJumpImpulse(new Vector2(0f, 18f));
contexts.config.SetJumpAcceleration(new Vector2(20f, -18f));
contexts.config.SetJumpMaxVelocity(new Vector2(20f, 45f));
contexts.config.SetMaxJumpCount(2);
contexts.config.SetMaxJumpTime(20f);
contexts.config.SetJumpEndOnStuckTimeout(0.1f);
contexts.config.SetStandardAcceleration(new Vector2(20f, -6f));
contexts.config.SetStandardMaxVelocity(new Vector2(20f, 70f));
contexts.config.SetAdditionalMidAirGravity(-35f);
contexts.config.SetVelocityIncreaseSpeed(80f);
contexts.config.SetVelocityDecreaseSpeed(2f);
contexts.config.SetStartPlayerPosition(new Vector2(0f, 4f));
contexts.config.SetPlayerSphereRadius(0.52f);
contexts.config.SetPlayerRespawnDelay(2f);
contexts.config.SetDeathHeight(-5f);
}
这种写法也是值得学习的
输入部分
输入部分分三个系统
public class InputSystems : Feature
{
public InputSystems(Contexts contexts, Services services)
{
Add(new InitPointerSystem(contexts));
Add(new UpdateTimeSystem(contexts, services));
Add(new UpdatePointerSystem(contexts, services));
}
}
InitPointerSystem
public sealed class InitPointerSystem : IInitializeSystem
{
private readonly Contexts _contexts;
public InitPointerSystem(Contexts contexts)
{
_contexts = contexts;
}
public void Initialize()
{
var e = _contexts.input.CreateEntity();
e.isLeftSidePointer = true;
AddCommon(e);
e = _contexts.input.CreateEntity();
e.isRightSidePointer = true;
AddCommon(e);
}
void AddCommon(InputEntity e)
{
e.AddPointerHoldingTime(0f);
e.isPointerStartedHolding = false;
e.isPointerHolding = false;
e.isPointerReleased = false;
}
}
在初始化的时候创建了两个Unique的实体,并对其相关属性赋值,他们一个代表是左键的相关操作
,一个代表了右键的相关操作。
关于左右键按住的组件和系统,工程放到了一个文件夹
在一开始对框架不是很熟悉的情况下,想知道某些实体里面有哪些组件是比较困难的,这样做对框架新手
比较友好。这里对于左右手鼠标按下行为,分别定义了左手还是右手标志位,开始按住标志位,松开标志位,正在按住标志位,按住时间标志位。
UpdateTimeSystem
public sealed class UpdateTimeSystem : IExecuteSystem, IInitializeSystem
{
private readonly Contexts _contexts;
private readonly ITimeService _timeService;
public UpdateTimeSystem(Contexts contexts, Services services)
{
_contexts = contexts;
_timeService = services.TimeService;
}
public void Initialize()
{
//Make it bulletproof
Execute();
}
public void Execute()
{
_contexts.input.ReplaceFixedDeltaTime(_timeService.FixedDeltaTime());
_contexts.input.ReplaceDeltaTime(_timeService.DeltaTime());
_contexts.input.ReplaceRealtimeSinceStartup(_timeService.RealtimeSinceStartup());
}
}
public sealed class UnityTimeService : Service, ITimeService
{
public UnityTimeService(Contexts contexts) : base(contexts)
{
}
public float FixedDeltaTime()
{
return Time.fixedDeltaTime;
}
public float DeltaTime()
{
return Time.deltaTime;
}
public float RealtimeSinceStartup()
{
return Time.realtimeSinceStartup;
}
}
在每帧中,获取Service封装好的时间相关属性刷新框架内的时间相关属性。并在初始化的时候就先进行了一次赋值,防止初始化时候其他系统需要用到但没有值。
UpdatePointerSystem
主要利用相关Service获得Unity提供的各种鼠标事件的信息。并实时刷新相关实体
public sealed class UpdatePointerSystem : IExecuteSystem
{
private readonly Contexts _contexts;
private readonly IInputService _inputService;
public UpdatePointerSystem(Contexts contexts, Services services)
{
_contexts = contexts;
_inputService = services.InputService;
}
public void Execute()
{
_inputService.Update(_contexts.input.deltaTime.value);
var left = _contexts.input.leftSidePointerEntity;
left.isPointerHolding = _inputService.IsHoldingLeft();
left.isPointerStartedHolding = _inputService.IsStartedHoldingLeft();
left.isPointerReleased = _inputService.IsReleasedLeft();
left.ReplacePointerHoldingTime(_inputService.HoldingTimeLeft());
var right = _contexts.input.rightSidePointerEntity;
right.isPointerHolding = _inputService.IsHoldingRight();
right.isPointerStartedHolding = _inputService.IsStartedHoldingRight();
right.isPointerReleased = _inputService.IsReleasedRight();
right.ReplacePointerHoldingTime(_inputService.HoldingTimeRight());
}
}
public interface IInputService
{
bool IsHoldingLeft();
bool IsStartedHoldingLeft();
float HoldingTimeLeft();
bool IsReleasedLeft();
bool IsHoldingRight();
bool IsStartedHoldingRight();
float HoldingTimeRight();
bool IsReleasedRight();
void Update(float delta);
}
using UnityEngine;
public sealed class UnityInputService : Service, IInputService
{
private float _holdingTimeLeft;
private bool _isHoldingLeft;
private bool _isReleasedLeft;
private bool _isStartedHoldingLeft;
private float _holdingTimeRight;
private bool _isHoldingRight;
private bool _isReleasedRight;
private bool _isStartedHoldingRight;
public UnityInputService(Contexts contexts) : base(contexts)
{
}
public void Update(float delta)
{
var midPoint = Screen.width / 2f;
var leftHitCounter = 0;
var rightHitCounter = 0;
#region Mouse
if (Input.GetMouseButton(0))
{
if (Input.mousePosition.x < midPoint)
{
leftHitCounter++;
}
else
{
rightHitCounter++;
}
}
#endregion
#region Keyboard
if (Input.GetKey(KeyCode.Space))
{
leftHitCounter++;
}
if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow))
{
rightHitCounter++;
}
#endregion
#region Touch
foreach (var touch in Input.touches)
{
if (touch.position.x < midPoint)
{
leftHitCounter++;
}
else
{
rightHitCounter++;
}
}
#endregion
if (leftHitCounter > 0)
{
if (_isHoldingLeft)
{
_holdingTimeLeft += delta;
_isStartedHoldingLeft = false;
}
else
{
_holdingTimeLeft = 0f;
_isStartedHoldingLeft = true;
}
_isHoldingLeft = true;
_isReleasedLeft = false;
}
else
{
if (_isHoldingLeft)
{
_isHoldingLeft = false;
_isReleasedLeft = true;
}
else
{
_isReleasedLeft = false;
}
}
if (rightHitCounter > 0)
{
if (_isHoldingRight)
{
_holdingTimeRight += delta;
_isStartedHoldingRight = false;
}
else
{
_holdingTimeRight = 0f;
_isStartedHoldingRight = true;
}
_isHoldingRight = true;
_isReleasedRight = false;
}
else
{
if (_isHoldingRight)
{
_isHoldingRight = false;
_isReleasedRight = true;
}
else
{
_isReleasedRight = false;
}
}
}
...
}
IsStartHoldingXXX是在某键按下的第一帧才有效