横版移动射击游戏(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; } }
|
项目流程五-子弹的载入和特效拖尾
先创建一个空对象,实现一个较为重要的功能。全局后处理,在Unity
中,它是实现后处理效果的一个重要工具,它可以应用于整个场景的后处理效果,比如色彩调整、景深、体积雾等。
Unity
中的后处理效果(Post-Processing Effects
)是一种通过在场景渲染完成后对图像进行处理,以提升画面视觉效果的技术。后处理效果可以为场景添加各种特效,使游戏画面更加真实或富有艺术感。Unity
的后处理通常通过后处理栈(Post-Processing Stack
)实现,支持一系列常用的效果:
在创建完成的组件中,创建一个自带的URP
组件,”Volume
“ 指的是一个用于控制场景中全局后处理效果的组件。后处理效果是在渲染完所有场景元素后,对最终图像进行的一系列处理,以增强视觉效果,比如添加模糊、色彩校正、HDR
效果等。
对主摄像机我们进行一些设置,和之后的URP
配置有很大关系,在相机组件中的Rendering
部分,我们调整抗锯齿的选项,调整为SMAF
。Anti-aliasing
是抗锯齿设置,用于减少图像中的锯齿边缘。在这里,它被设置为”Subpixel Morphological Anti-aliasing (SMAA)
“,这是一种高效的抗锯齿技术。
在选取environment
部分,我们将天空盒关闭。天空盒(Skybox
) 是一种特殊的材质,用于模拟场景中的天空或环境背景。它通过将场景包裹在一个虚拟的盒子或球体中,使玩家感觉场景被无限大的天空或背景所围绕,从而增强沉浸感。Culling Mask
这个选项允许你选择相机渲染时应该包含或排除哪些层(Layer
)。在这里,它被设置为”Everything
“,意味着相机将渲染所有层。
点击上面的new
创建一个组件,这里的Mode
设置为Global
就是全局显示后处理。局部的后处理效果需要添加碰撞体,常用于渲染水的波形,还有其他一些光效。在战机中添加一些后处理效果
Bloom
是一种后处理效果,用于模拟相机在高光区域过度曝光时产生的光晕效果。这在现实世界中常见于非常明亮的光源,如太阳光或车灯。Bloom
效果可以增强场景的视觉冲击力,使高光部分更加突出。以下是Bloom
组件中的各个设置:
- Threshold: 泛光效果开始的阈值。只有亮度高于这个阈值的像素才会被处理。
- Intensity: 泛光效果的强度。值越高,泛光效果越明显。
- Scatter: 控制泛光的散射程度,影响泛光的扩散范围。
- Tint: 泛光的颜色。可以调整泛光的颜色,使其与场景的整体色调相匹配。
- Clamp: 限制泛光效果的最大亮度,防止过度曝光。
- High Quality Filtering: 启用高质量过滤,可以提高泛光效果的质量,但可能会增加计算量。
Lens Dirt
是一种模拟真实相机镜头上的污垢、划痕或其他瑕疵的后处理效果。这种效果可以为游戏或电影场景添加一种真实感或特定的风格。以下是Lens Dirt
组件中的各个设置:
- Dirt Texture: 用于镜头污垢效果的纹理。这个纹理包含了污垢、划痕等图案,可以被映射到镜头上。
- Dirt Intensity: 污垢效果的强度。值越高,污垢效果越明显。
在Unity
中,”Tonemapping
“(色调映射)是一种后处理技术,用于调整图像的亮度和对比度,使其适应不同的显示设备和观看条件。色调映射是HDR
(高动态范围)图像渲染过程中的一个重要步骤,它将HDR
图像的亮度范围压缩到适合显示设备的范围内。
在Unity
中,”Color Adjustments
“(颜色调整)是一组后处理效果,用于调整场景中图像的颜色特性。这些调整可以增强视觉效果,或者为场景提供特定的视觉风格。以下是颜色调整组件中的各个设置:
- Post Exposure: 后期曝光调整。这个设置允许你调整图像的整体亮度,通过增加或减少曝光量来实现。这里后期曝光被设置为-0.26,这意味着图像将稍微变暗。
- Contrast: 对比度调整。这个设置可以增加或减少图像的对比度,使得亮部更亮,暗部更暗。
- Color Filter: 颜色滤镜。这个设置允许你通过选择不同的颜色滤镜来改变整个场景的颜色。在你提供的内容中,颜色滤镜被设置为”
HDR
“,应用了一种高动态范围的颜色滤镜。
- Hue Shift: 色调偏移。这个设置允许你调整图像的色调,使得颜色向色轮上的不同方向偏移。色调偏移被设置为7,使得图像的色调稍微偏移。
- Saturation: 饱和度调整。这个设置可以增加或减少图像中颜色的饱和度,使得颜色更加鲜艳或更加柔和。饱和度被设置为22,将变得更加鲜艳。
在Unity
中,”White Balance
“(白平衡)是颜色调整后处理效果的一部分,用于校正图像中的色温,使得白色物体在不同光照条件下看起来仍然是白色的。这是通过调整图像的色温和色调来实现的。以下是白平衡组件中的各个设置:
- Temperature(色温): 这个设置控制图像的色温,即图像的冷暖程度。较低的色温值会使图像偏蓝(冷色调),而较高的色温值会使图像偏黄或红(暖色调)。色温被设置为-16,使图像偏向冷色调。
- Tint(色调): 这个设置控制图像的绿色-品红色平衡。正值会增加图像的品红色调,而负值会增加绿色调。色调被设置为16,使图像偏向品红色调。
在Unity
中,”Vignette
“(暗角)是一种后处理效果,用于在图像的边缘添加渐变的暗区,使得观众的注意力更加集中在图像的中心。这种效果可以用于创造特定的视觉效果,或者模拟某些镜头的光学特性。以下是暗角组件中的各个设置:
- Color: 暗角的颜色。这个设置允许你选择暗角区域的颜色,通常用于调整暗角效果的整体色调。
- Center: 暗角效果的中心点。在Unity中,这个设置通常用于定义暗角效果开始的区域。中心点的X和Y坐标都被设置为0.5,这意味着暗角效果将从图像的中心开始。
- Intensity: 暗角的强度。这个设置控制暗角效果的明显程度。强度被设置为0.206,这将使得暗角效果较为微妙。
- Smoothness: 暗角边缘的平滑度。这个设置用于调整暗角区域与图像中心区域之间的过渡平滑度。平滑度被设置为0.242,这将使得暗角效果的边缘较为柔和。
- Rounded: 这个选项可能用于控制暗角效果的形状是否为圆形。这里没有选取意味着暗角效果可能是方形的。
在设置完成上面的环境,之后的环境变化就更像深空了,所以说后处理效果是对环境的渲染效果。
接下来创建一个文件夹容纳Projectile
,制作战机的子弹。导入油管主的包
将子弹模型的物体shader
消除,这里的materials
将mode选择为none
将提供的纹理贴图变为透明的贴图,设置如下的选择。
创建一个对象命名为Player Projectile
,创建一个子对象命名为粒子效果。在粒子效果中加入一个Particle System
渲染粒子效果,将油管主提供的模型和纹理贴图添加到这个粒子效果中。
在这个Particle System
中,修改Render Mode
属性,修改为Mesh
将子弹的Mesh
添加到Meshes
中。接下来依靠提供的素材制作一个材质
在创建的材质中,选择universal Render Pipeline -> Particles -> unlit
将做好的材质贴图添加到这个材质当中保存,子弹的效果就会随着这个材质贴图变化。
为了拿出单个子弹,我们对其粒子效果进行修改。这里主要的是将Rate Over Time
修改为2,就会产生单个子弹。在初始大小的时候调整为0.5。
将Color over Lifetime
居中的color
的伽马值修改为0,这样就得到了一个闪烁的子弹效果。
通过将Rotation over Lifetime
修改其旋转的轴,得到子弹旋转效果。这样子弹的制作就基本完成。主要是设置这个shader
比较麻烦。
接下来制作子弹尾迹效果。创建一个effect->tail
物体
将之前创建的材质进行拷贝,命名规范
将作为尾迹的材质中的蒙皮删除
在子弹尾迹中,修改下列划横线的参数,添加我们复制的材质
这样就实现了尾迹效果
接下来让子弹可以从战机中发射出来,通过设计一个子弹发射的基类,方便敌人和战机继承这个类从而简化操作。
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
| using System.Collections; using UnityEngine;
public class Projectile : MonoBehaviour {
[SerializeField] float moveSpeed = 10f;
[SerializeField] Vector2 moveDirection;
void OnEnable() { StartCoroutine(MoveDirectly()); }
IEnumerator MoveDirectly() { while (gameObject.activeSelf) { transform.Translate(moveDirection * moveSpeed * Time.deltaTime);
yield return null; } } }
|
在子弹组件中创建一个脚本,PlayerProjectile
在这里继承上面的类
1 2 3 4 5 6
| using UnityEngine;
public class PlayerProjectile: Projectile { }
|
在子弹组件中奖这个移动速度修改为1,子弹就可以移动了。