横版移动射击游戏(2)
项目流程三-限制视窗
在之前的项目流程(3)中,实现了主角的移动效果。但是出现玩家移动到镜头外面的情况,可以通过限制玩家的移动效果来达到。这里通过单例的方式,创建一个脚本用来控制environment
为了解耦和复用,创建一个类供给viewport
来继承,通过这个类,可以在创建对象时自动生成一个 instance
对象。继承该类的子类,,在创建对象时也能拥有一个 instance
对象实现了单例的效果。使用泛型单例模式的最大优势之一就是,它自动管理单例实例,这样你就无需在主程序或其他地方手动创建 Instance
对象了。
1 2 3 4 5 6 7 8 9 10 11 12 13
| using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : Component { public static T Instance { get; private set;}
protected virtual void Awake() { Instance = this as T; } }
|
然后在挂载的environment
中编写程序,根据下面的模型图,编写了限制范围。
在 Unity 中,搭载了 Transform 组件的对象具有以下属性:
Pivot(旋转中心点):
Pivot
是 3D 模型旋转的中心点。在 Unity
中,你可以设置模型的 Pivot
点,模型将围绕此点进行旋转。图中绿色线标出的是飞机模型的旋转中心。
PaddingX:
PaddingX
是指在 X 轴方向上增加的填充距离。在使用物理引擎或碰撞检测时,可能需要在模型边界之外增加一定的空间,以避免模型之间的穿透或提高物理交互的稳定性。图中用红色箭头表示了 X 轴方向的填充距离。
PaddingY:
PaddingY
是指在 Y 轴方向上增加的填充距离。与 PaddingX
类似,它用于在 Y 轴方向上增加额外的空间,从而提高物理交互的稳定性。图中用绿色箭头表示了 Y 轴方向的填充距离。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| using UnityEngine; using UnityEngine.UIElements;
public class Viewport: Singleton<Viewport> {
float minX;
float maxX;
float minY;
float maxY;
void Start() { Camera mainCamera = Camera.main;
Vector2 bottomLeft = mainCamera.ViewportToWorldPoint(new Vector3(0f,0f)); Vector2 topRight = mainCamera.ViewportToWorldPoint(new Vector3(1f,1f));
minX = bottomLeft.x; minY = bottomLeft.y; maxX = topRight.x; maxY = topRight.y; }
public Vector3 PlayerMoveablePosition(Vector3 playerPosition, float paddingX, float paddingY) { Vector3 position = Vector3.zero;
position.x = Mathf.Clamp(playerPosition.x, minX + paddingX, maxX - paddingX); position.y = Mathf.Clamp(playerPosition.y, minY + paddingY, maxY - paddingY);
return position; }
}
|
在player
中调用上面声明的对象,将设置好的变量带入到函数之中。这段代码通过使用协程实现了一个无限循环的过程,用于实时获取当前对象的位置并对其进行限制。具体来说,这个协程在每一帧更新物体的位置,以确保它始终位于允许的范围内。
协程的作用是在 Unity 中实现延时操作或分步执行,并且不阻塞主线程。它允许你在多个时间点之间暂停执行某些任务,并在合适的时候恢复执行。常用于处理时间相关的操作,如动画、等待、异步任务等。想象你在一个银行排队办理业务。排队的过程就像是协程的执行:
- 排队(协程开始):你排队等待办理业务,但你不会一直站着不动。你可以在排队时做其他事,比如看看手机或和旁边的人聊天,这就类似于协程中“暂停”执行的状态。
- 等待(暂停执行):假设有些事务需要时间,比如银行工作人员在为前一个客户办理业务,你只能站在那里等待。这时,虽然你在等待,但并没有浪费时间,其他事情(比如其他客户办理业务)仍在继续进行。协程也会在“等待”某些条件时暂停,并允许其他代码执行。
- 被叫到窗口(继续执行):当轮到你办理业务时,银行工作人员叫你过去,你继续处理自己的事务。此时,你的操作就像是协程恢复执行,继续完成接下来的任务。
- 业务完成(协程结束):一旦业务完成,你可以离开排队。协程也是如此,完成所有操作后自然结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| using System.Collections; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.Scripting.APIUpdating;
[RequireComponent(typeof(Rigidbody2D))] public class Player : MonoBehaviour { [SerializeField] PlayerInput input; [SerializeField] float moveSpeed = 10f; [SerializeField] float paddingX; [SerializeField] float paddingY;
new Rigidbody2D rigidbody;
[System.Obsolete] void OnEnable() { input.onMove += Move; input.onStopMove += StopMove; }
[System.Obsolete] void OnDisable() { input.onMove -= Move; input.onStopMove -= StopMove; }
void Start() { rigidbody = GetComponent<Rigidbody2D>(); rigidbody.gravityScale = 0f;
input.EnableGameplayInput(); }
[System.Obsolete] void Move(Vector2 moveInput) { rigidbody.velocity = moveInput * moveSpeed; StartCoroutine(MovePositionLimitCoroutine()); } [System.Obsolete] void StopMove() { rigidbody.velocity = Vector2.zero; StopCoroutine(MovePositionLimitCoroutine());
} IEnumerator MovePositionLimitCoroutine() { while(true) { transform.position = Viewport.Instance.PlayerMoveablePosition(transform.position, paddingX, paddingY); yield return null; } }
}
|
项目流程四-更改加速逻辑
在上述流程中,我们的加速逻辑存在一些问题,主要表现为速度的突变。为了更符合现实物理规律,速度不应突变,因此我们应该采用加速度来衡量运动的变化。在现实世界中,物体的速度变化是连续的,不会出现突变。为了实现平滑的加速效果,我们可以使用 lerp
函数,通过线性插值来平滑地调整加速度,从而避免速度的突变。
在上面的代码中,我们定义了加速时间和减速时间,并规定在一定的过渡时间后才能达到最大加速度。我们使用协程来平滑地干预战机的加速度变化。使用协程的主要目的是便于状态切换。例如,当战机正在向上飞行时,转向向下飞行时,协程可以暂停当前的向上飞行过程,并执行向下飞行的加速,确保加速度的平滑过渡。如果使用普通函数来实现这一点,函数会在每一帧内逐步改变加速度,导致状态切换时必须等待当前加速完成后才能执行新方向的加速,这样会造成加速度的突变,失去平滑的过渡效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| IEnumerator MoveCoroutine(float time, Vector2 moveVelocity) { float t = 0f; Vector2 initialVelocity = rigidbody.linearVelocity;
while (t < time) { t += Time.fixedDeltaTime; float lerpFactor = t / time;
rigidbody.linearVelocity = Vector2.Lerp(initialVelocity, moveVelocity, lerpFactor);
yield return null; }
rigidbody.linearVelocity = moveVelocity; yield return null; }
|
同时为了提高移动的视觉效果,在上下移动的时候加入翻转的效果。向上移动的时候改变transform
中的Rotation
中的x
轴变为正值,向下移动的时候相反实现翻转的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| void Move(Vector2 moveInput) { if (moveCoroutine != null) { StopCoroutine(moveCoroutine); }
moveCoroutine = StartCoroutine(MoveCoroutine(accelerationTime, moveInput.normalized * moveSpeed)); StartCoroutine(MovePositionLimitCoroutine());
if (moveInput.y > 0) { transform.eulerAngles = new Vector3(tiltAngle, transform.eulerAngles.y, transform.eulerAngles.z); } else if (moveInput.y < 0) { transform.eulerAngles = new Vector3(-tiltAngle, transform.eulerAngles.y, transform.eulerAngles.z); } }
void StopMove() { if (moveCoroutine != null) { StopCoroutine(moveCoroutine); }
moveCoroutine = StartCoroutine(MoveCoroutine(decelerationTime, Vector2.zero));
StopCoroutine(MovePositionLimitCoroutine());
transform.eulerAngles = new Vector3(0, transform.eulerAngles.y, transform.eulerAngles.z); }
|
这里介绍一下两个比较常用的函数。
StartCoroutine
:启动一个协程,让任务在多个帧内执行,不会阻塞主线程。调用带有 IEnumerator
的方法启动协程。使用方向一般是实现延迟、平滑动画、逐帧更新。
1
| StartCoroutine(MyCoroutine());
|
StopCoroutine
:停止一个正在运行的协程。通过协程实例或方法名称停止协程。使用方向一般是状态切换或条件改变时停止协程,防止多个协程冲突。
1
| StopCoroutine(myCoroutine);
|
简而言之,StartCoroutine
用来启动协程,StopCoroutine
用来停止协程。今天完整的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| using System.Collections; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.Scripting.APIUpdating;
[RequireComponent(typeof(Rigidbody2D))] public class Player : MonoBehaviour { [SerializeField] PlayerInput input; [SerializeField] float moveSpeed = 10f; [SerializeField] float paddingX; [SerializeField] float paddingY; [SerializeField] float accelerationTime = 3f; [SerializeField] float decelerationTime = 3f; [SerializeField] float tiltAngle;
new Rigidbody2D rigidbody; Coroutine moveCoroutine;
void OnEnable() { input.onMove += Move; input.onStopMove += StopMove; }
void OnDisable() { input.onMove -= Move; input.onStopMove -= StopMove; }
void Start() { rigidbody = GetComponent<Rigidbody2D>(); rigidbody.gravityScale = 0f;
input.EnableGameplayInput(); }
void Move(Vector2 moveInput) { if (moveCoroutine != null) { StopCoroutine(moveCoroutine); }
moveCoroutine = StartCoroutine(MoveCoroutine(accelerationTime, moveInput.normalized * moveSpeed)); StartCoroutine(MovePositionLimitCoroutine());
if (moveInput.y > 0) { transform.eulerAngles = new Vector3(tiltAngle, transform.eulerAngles.y, transform.eulerAngles.z); } else if (moveInput.y < 0) { transform.eulerAngles = new Vector3(-tiltAngle, transform.eulerAngles.y, transform.eulerAngles.z); } }
void StopMove() { if (moveCoroutine != null) { StopCoroutine(moveCoroutine); }
moveCoroutine = StartCoroutine(MoveCoroutine(decelerationTime, Vector2.zero));
StopCoroutine(MovePositionLimitCoroutine());
transform.eulerAngles = new Vector3(0, transform.eulerAngles.y, transform.eulerAngles.z); }
IEnumerator MovePositionLimitCoroutine() { while (true) { transform.position = Viewport.Instance.PlayerMoveablePosition(transform.position, paddingX, paddingY); yield return null; } }
IEnumerator MoveCoroutine(float time, Vector2 moveVelocity) { float t = 0f; Vector2 initialVelocity = rigidbody.velocity;
while (t < time) { t += Time.fixedDeltaTime; float lerpFactor = t / time;
rigidbody.velocity = Vector2.Lerp(initialVelocity, moveVelocity, lerpFactor);
yield return null; }
rigidbody.velocity = moveVelocity; yield return null; } }
|
项目流程五-子弹的载入和移动