对于那些错过了第一部分的人-
第一部分下一部分-
第3部分如果有人有兴趣阅读有关该事件使用的聚合器的信息,那么
这里就是您,但这不是必需的。
因此,我们开始将所有内容收集到一个堆中
火箭:基础火箭级using DG.Tweening; using GlobalEventAggregator; using UnityEngine; namespace PlayerRocket { public class Rocket : PlayerRocketBase { [SerializeField] private float pathorrectionTime = 10; private Vector3 movingUp = new Vector3(0, 1, 0); protected override void StartEventReact(ButtonStartPressed buttonStartPressed) { transform.SetParent(null); rocketState = RocketState.MOVE; transform.DORotate(Vector3.zero, pathorrectionTime); } protected override void Start() { base.Start(); EventAggregator.Invoke(new RegisterUser { playerHelper = this }); if (rocketState == RocketState.WAITFORSTART) return; RocketBehaviour(); } private void FixedUpdate() { RocketBehaviour(); } private void RocketBehaviour() { switch (rocketState) { case RocketState.WAITFORSTART: if (inputController.OnTouch && !inputController.OnDrag) rocketHolder.RotateHolder(inputController.worldMousePos); break; case RocketState.MOVE: rigidbody.AddRelativeForce(Vector3.up*(config.Speed*Time.deltaTime)); forceModel.AddModificator(); break; case RocketState.STOP: Debug.Log(" "); rigidbody.velocity = Vector3.zero; rigidbody.drag = 50; rocketState = RocketState.COMPLETESTOP; break; case RocketState.COMPLETESTOP: break; default: rocketState = RocketState.COMPLETESTOP; break; } } } }
要让火箭起飞,我们需要什么? 在游戏空间中,我们需要一个有条件的行星来启动,一个启动按钮和一个火箭。 火箭应该做什么?
- 等待开始
- 飞
- 受到修饰符的影响
- 停止
也就是说,我们有不同的火箭行为/状态,根据当前状态,火箭应提供不同的行为。 在编程中,我们经常遇到对象可能具有许多根本不同的行为的情况。
对于对象的复杂行为-最好使用行为模式,例如状态模式。 对于简单的程序员,如果不是这样的话,新手程序员通常会大量使用。 我建议使用开关和枚举。 首先,这是将逻辑更明确地划分为特定的阶段,因此,我们可以肯定地知道我们现在所处的状态以及正在发生的事情,将代码变成数十种异常情况的机会更少。
运作方式:首先,我们从需要的状态开始枚举:
public enum RocketState { WAITFORSTART = 0, MOVE = 1, STOP = 2, COMPLETESTOP = 3, }
在父类中,我们有一个字段-
protected RocketState rocketState;
默认情况下,第一个值被分配给它。 枚举本身设置默认值,但对于可以从上方更改或由游戏设计师配置的数据-我手动设置这些值,原因是什么? 为了能够在其他地方为inam添加另一个值,并且不违反存储的数据。 我还建议您学习标志枚举。
下一个:我们根据rocketState字段的值在更新中定义行为本身
private void FixedUpdate() { RocketBehaviour(); } private void RocketBehaviour() { switch (rocketState) { case RocketState.WAITFORSTART: if (inputController.OnTouch && !inputController.OnDrag) rocketHolder.RotateHolder(inputController.worldMousePos); break; case RocketState.MOVE: rigidbody.AddRelativeForce(Vector3.up*(config.Speed*Time.deltaTime)); forceModel.AddModificator(); break; case RocketState.STOP: Debug.Log(" "); rigidbody.velocity = Vector3.zero; rigidbody.drag = 50; rocketState = RocketState.COMPLETESTOP; break; case RocketState.COMPLETESTOP: break; default: rocketState = RocketState.COMPLETESTOP; break; } }
我将解释正在发生的事情:- 等待时,我们只需将火箭朝着鼠标光标旋转,从而设置初始轨迹
- 第二种状态-我们飞行,朝正确的方向加速火箭,并更新修改器模型,以影响物体的轨迹
- 第三种状态是当团队到达我们要停止的位置时,我们在这里进行所有工作,以使火箭停止并转化为状态-我们完全停止了。
- 最后一个状态是我们什么都不做。
当前模式的便利性-都非常容易扩展和调整,但是只有一个弱环节-这是我们可以拥有一个将多个其他状态组合在一起的状态的时候。 在这里,要么是标记inam,处理复杂,要么就是已经切换到更多“繁重”的模式。
我们弄清楚了火箭。 下一步是一个简单但有趣的对象-“开始”按钮。
开始按钮
她需要以下功能-单击后,她通知他们单击了她。
开始按钮类 using UnityEngine; using UnityEngine.EventSystems; public class StartButton : MonoBehaviour, IPointerDownHandler { private bool isTriggered; private void ButtonStartPressed() { if (isTriggered) return; isTriggered = true; GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed()); Debug.Log(""); } public void OnPointerDown(PointerEventData eventData) { ButtonStartPressed(); } } public struct ButtonStartPressed { }
根据游戏设计,这是舞台上的3D对象,应该将按钮集成到起始行星的设计中。 好吧,好的,有一个细微差别-如何跟踪场景中某个对象的点击?
如果我们使用google,我们会发现一堆OnMouse方法,其中将有一个单击。 看来这是一个简单的选择,但它非常糟糕,首先是因为它经常会歪曲地运行(跟踪点击有许多细微差别),“亲爱的”,最后是它没有提供UnityEngine.EventSystems中的大量面包。
最后,我建议使用UnityEngine.EventSystems和接口IPointerDownHandler,IPointerClickHandler。 在他们的方法中,我们认识到了对压力的反应,但是有一些细微差别。
- EventSystem必须存在于场景中,这是单元的对象/类/组件,通常在我们为接口创建画布时创建,但是您也可以自己创建它。
- 相机上必须存在物理RayCaster(用于3D,对于2D图形,有一个单独的racaster)
- 设施必须有对撞机
在项目中,它看起来像这样:
现在,对象跟踪点击,此方法称为:
public void OnPointerDown(PointerEventData eventData) { ButtonStartPressed(); } private void ButtonStartPressed() { if (isTriggered) return; isTriggered = true; GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed()); Debug.Log(""); }
这是怎么回事:我们有一个布尔字段,可在其中跟踪按钮是否被按下(这是防止重复按下的保护,因此我们不会每次都运行启动脚本)。
接下来,我们将事件称为事件-按下按钮,订阅火箭类,并使该按钮处于运动状态。
向前跳一点-为什么在这里和那里发生事件? 这是一个面向事件的编程,首先,事件模型比连续数据处理便宜,以便找出它们的变化。 其次,这是最弱的联系,我们不需要在火箭上知道有一个按钮,有人按下了按钮,依此类推,我们只知道有一个事件要开始,我们收到并正在采取行动。 此外,此事件不仅对于火箭来说很有趣,例如,针对同一事件对带有修饰符的面板进行签名,并且在火箭开始时将其隐藏。 同样,此事件可能是输入控制器感兴趣的-火箭发射后,用户输入可能未得到处理或未进行其他处理。
为什么很多程序员都不喜欢事件范例? 由于大量事件和对这些事件的订阅很容易将代码变成面条,因此根本不清楚从何处开始以及代码是否会在某处结束,更不用说您还需要监视您的取消订阅/订阅并使所有对象保持活动的事实。
这就是为什么在事件的实现中使用事件聚合器的原因,该事件聚合器实际上不传输事件,而是通过事件传递数据容器,并且类订阅对它们感兴趣的数据。 而且,聚合器本身监视活动对象并将不活动的对象扔出订阅者。 由于容器的转移,注入也是可能的;您可以将链接传递给我们感兴趣的类。 通过容器,您可以轻松跟踪谁处理和发送此数据。 对于原型设计是一件好事。
火箭旋转确定起始路径

根据游戏设计,火箭应该能够绕行星旋转以确定初始轨迹,但不能超过某个角度。 旋转是通过触摸进行的-火箭只需跟随手指,并始终指向我们在屏幕上戳的地方。 顺便说一句,正是原型使我们有可能确定这是一个弱点,并且与管理相关的许多事件都与此功能接壤。
但是为了:- 我们需要火箭沿着独轮车的方向相对于行星转动
- 我们需要固定旋转角度
至于相对于行星的旋转-您可以巧妙地绕轴旋转并计算旋转轴,或者可以简单地创建一个对象,该对象的假人位于行星内部,然后将火箭移动到该位置,然后使假人绕Z轴安静地旋转,该假人将具有一个确定对象行为的类。 火箭将随之旋转。 我称为RocketHolder的对象。 我们知道了。
现在,关于在独轮车方向上转弯的限制:
类RocketHolder using UnityEngine; public class RocketHolder : MonoBehaviour { [SerializeField] private float clampAngle = 45; private void Awake() { GlobalEventAggregator.EventAggregator.AddListener(this, (InjectEvent<RocketHolder> obj) => obj.inject(this)); } private float ClampAngle(float angle, float from, float to) { if (angle < 0f) angle = 360 + angle; if (angle > 180f) return Mathf.Max(angle, 360 + from); return Mathf.Min(angle, to); } private Vector3 ClampRotationVectorZ (Vector3 rotation ) { return new Vector3(rotation.x, rotation.y, ClampAngle(rotation.z, -clampAngle, clampAngle)); } public void RotateHolder(Vector3 targetPosition) { var diff = targetPosition - transform.position; diff.Normalize(); float rot_z = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg; transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90); transform.eulerAngles = ClampRotationVectorZ(transform.rotation.eulerAngles); } }
尽管游戏理论上是3D的,但整个逻辑和游戏玩法实际上是2D。 而且,我们只需要沿加压位置的方向绕Z轴旋转火箭即可。 在方法的最后,我们将旋转度限制为检查器中指定的值。 在Awake方法中,您可以看到通过聚合器进行类注入的最正确实现。
输入控制器
最重要的类之一是收集和处理用户行为的他。 按下热键,游戏手柄按钮,键盘等。 我在原型中输入的内容非常简单,实际上您只需要了解3件事:
- 是否有点击及其坐标
- 是否有垂直滑动以及滑动多少
- 我是否可以使用界面/修饰符
InputController类 using System; using UnityEngine; using UnityEngine.EventSystems; public class InputController : MonoBehaviour { public const float DirectionRange = 10; private Vector3 clickedPosition; [Header(" ")] [SerializeField] private float afterThisDistanceWeGonnaDoSwipe = 0.5f; [Header(" ")] [SerializeField] private float speedOfVerticalScroll = 2; public ReactiveValue<float> ReactiveVerticalScroll { get; private set; } public Vector3 worldMousePos => Camera.main.ScreenToWorldPoint(Input.mousePosition); public bool OnTouch { get; private set; } public bool OnDrag { get; private set; }
一切都在额头上,没有麻烦,有趣的可能是响应式专有的原始实现-当我刚开始编程时,如何发现数据已更改而又没有不断更新数据总是很有趣。 好吧,就是这样。
看起来像这样:
ReactiveValue类 public class ReactiveValue<T> where T: struct { private T currentState; public Action<T> OnChange; public T CurrentValue { get => currentState; set { if (value.Equals(currentState)) return; else { currentState = value; OnChange?.Invoke(currentState); } } } }
我们订阅OnChange,如果只有值更改,我们会抽搐。
关于原型和架构-提示是相同的,仅公开属性和方法,所有数据仅应在本地更改。 任何处理和计算-根据不同的方法相加。 结果,您可以随时更改实现/计算,并且这不会影响该类的外部用户。 至此,在第三部分的最后-有关修饰符和界面(拖放)。 我计划将项目放在git上,以便我可以看到/感觉到。 如果您对原型有疑问-请询问,我会尽力明确回答。