0
点赞
收藏
分享

微信扫一扫

Entitas学习[五]无尽小球游戏-输入部分和整体入口和整体配置部分

雷亚荣 2022-02-27 阅读 30

这个是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是在某键按下的第一帧才有效

举报

相关推荐

0 条评论