前言
- 这篇又是看完油管教程后写的,实现方式其实比较简单,但还是写写吧
- 唯一稍微有点难度的就是涉及到贝塞尔曲线,好在只有二阶
PS:关于shader(写过一篇)、粒子系统、Cinemachine等基础在本文不会涉及
效果预览
简约线条版(当前实现)
极致色彩版(模仿对象)
逐步实现
- 基础场景+移动脚本不再赘述,初始环境如下:
- 初始的核心脚本
ThrowController_Clone
如下:
using UnityEngine;
[RequireComponent(typeof(Animator))]
public class ThrowController_Clone : MonoBehaviour
{
private Animator animator; // 动画控制器
private MovementInput input; // 移动控制脚本
private Rigidbody weaponRb; // 斧头武器
[Tooltip("飞斧")]
public Transform weapon; // 飞斧
[Tooltip("拿武器的手,武器的父对象")]
public Transform hand; // 拿武器的手
[Space]
[Header("开关")]
public bool walking = true; // 是否在走路
// Start is called before the first frame update
void Start()
{
animator = GetComponent<Animator>();
input = GetComponent<MovementInput>();
weaponRb = weapon.GetComponent<Rigidbody>();
}
// Update is called once per frame
void Update()
{
// 根据状态设置动画
walking = input.Speed > 0;
animator.SetBool("walking", walking);
}
}
瞄准状态
- 实现的逻辑是要进入瞄准状态后才能飞出斧头,所以先把右键瞄准给实现了
- 首先准心的UI,就是一个小四方形:
- 我们需要:
- 让其在游戏开始时隐藏
- 在右键进入瞄准状态时才出现
- 进入瞄准状态时摄像机中心更贴近斧头
- 斧头一些特效
- 松开右键后退出锁定状态
- 代码变更如下:
public bool aiming = false; // 是否在瞄准
public float cameraZoomOffset = .3f; // 摄像机偏移
public ParticleSystem glowParticle; // 斧头环绕粒子
public Image reticle; // 准心
public CinemachineFreeLook virtualCamera; // 摄像机
void Start()
{
// 略......
// 隐藏准心
reticle.DOFade(0, 0);
}
void Update()
{
// 略......
// 鼠标右键+持有武器
if (Input.GetMouseButtonDown(1) && hasWeapon)
{
// 进入瞄准状态
Aim(true, true, 0);
}
// 没有扔出斧头就松开右键,退出瞄准状态
if (Input.GetMouseButtonUp(1) && hasWeapon)
{
Aim(false, true, 0);
}
}
/// <summary>
/// 设置瞄准状态(镜头贴近/远离角色)
/// </summary>
/// <param name="state">瞄准状态</param>
/// <param name="changeCamera">Z轴偏移</param>
/// <param name="delay">过渡时间</param>
void Aim(bool state, bool changeCamera, float delay)
{
if (walking)
return;
aiming = state;
animator.SetBool("aiming", aiming);
// 瞄准图标显现
float fade = state ? 1 : 0;
reticle.DOFade(fade, .2f);
if (!changeCamera)
return;
// 摄像机偏移
float newAim = state ? cameraZoomOffset : 0;
float originalAim = !state ? cameraZoomOffset : 0;
DOVirtual.Float(originalAim, newAim, .5f, CameraOffset).SetDelay(delay);
// 粒子特效
if (state)
{
glowParticle.Play();
}
else
{
glowParticle.Stop();
}
}
/// <summary>
/// 设置相机Top Rig、Middle Rig、Bottom Rig的偏移量
/// </summary>
/// <param name="offset"></param>
void CameraOffset(float offset)
{
virtualCamera.GetRig(0).GetCinemachineComponent<CinemachineComposer>().m_TrackedObjectOffset = new Vector3(offset, 1.5f, 0);
virtualCamera.GetRig(1).GetCinemachineComponent<CinemachineComposer>().m_TrackedObjectOffset = new Vector3(offset, 1.5f, 0);
virtualCamera.GetRig(2).GetCinemachineComponent<CinemachineComposer>().m_TrackedObjectOffset = new Vector3(offset, 1.5f, 0);
}
- 还有进入瞄准状态后不可移动的脚本就略过了,当前效果如下:
- 在确认姿势没问题后,再补上人物跟随摄像机旋转,时刻跟随准心的脚本:
void Update()
{
// 如果处于瞄准状态,则角色朝向跟随摄像头;否则角色朝向恢复不跟随摄像机
if (aiming)
{
input.RotateToCamera(transform);
}
else
{
transform.eulerAngles = new Vector3(Mathf.LerpAngle(transform.eulerAngles.x, 0, .2f), transform.eulerAngles.y, transform.eulerAngles.z);
}
// 略......
}
- 移动脚本中增加的实现:
/// <summary>
/// 角色朝向跟随摄像头(更改视角)
/// </summary>
/// <param name="t"></param>
public void RotateToCamera(Transform t)
{
var forward = cam.transform.forward;
forward.Normalize();
desiredMoveDirection = forward;
t.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(desiredMoveDirection), desiredRotationSpeed);
}
- 修改后效果如下:
飞出斧头
- 飞斧做的事情主要如下:
- 播放飞斧动画,在合适时机给斧头一个一次性的力推出去
- 飞的过程要旋转
- 碰到墙后插在墙上
- 首先时第一步,把斧头飞出去,在进入瞄准状态后,再鼠标左键就进行这个动作,代码改动如下:
public TrailRenderer trailRenderer; // 拖尾特效
public ParticleSystem trailParticle; // 拖尾粒子
public float throwPower = 240; // 扔出力度
void Update()
{
// 略......
if (hasWeapon)
{
// 如果持有斧头 & 瞄准状态 & 鼠标左键,播放扔斧头动画
if (aiming && Input.GetMouseButtonDown(0))
{
animator.SetTrigger("throw");
}
}
}
public void WeaponThrow()
{
// 退出锁定状态
Aim(false, true, 1f);
hasWeapon = false;
weaponRb.isKinematic = false;
weaponRb.collisionDetectionMode = CollisionDetectionMode.Continuous;
weapon.parent = null;
weaponRb.AddForce(Camera.main.transform.forward * throwPower + transform.up * 2, ForceMode.Impulse);
// 例子和拖尾特效
trailRenderer.emitting = true;
trailParticle.Play();
}
- 当前效果如下:
- 这里其实还有个小毛病,那就是斧头飞出去的时候,并没有对准准心(往左偏了不少)
- 我们可以稍微调整下斧子飞出去前的偏移:
public void WeaponThrow()
{
// 略......
weapon.transform.position += transform.right / 5;
weaponRb.AddForce(Camera.main.transform.forward * throwPower + transform.up * 2, ForceMode.Impulse);
}
- 修改后效果如下:
- 接下来就是实现斧头飞的过程中旋转了,新增
WeaponScript
实现如下:
using UnityEngine;
public class WeaponScript : MonoBehaviour
{
// 飞斧是否激活
public bool activated;
// 旋转速度
public float rotationSpeed;
void Update()
{
// 如果激活,则使用欧拉角旋转斧头
if (activated)
{
transform.localEulerAngles += Vector3.forward * rotationSpeed * Time.deltaTime;
}
}
private void OnCollisionEnter(Collision collision)
{
// 斧头碰到墙壁就停下(墙壁是11)
if (collision.gameObject.layer == 11)
{
print(collision.gameObject.name);
// 关闭物理模拟,节省计算资源(可被唤醒)
GetComponent<Rigidbody>().Sleep();
// 连续推测模式,减少开销,同时让武器插进去一点(但飞得过快可能会出问题)
GetComponent<Rigidbody>().collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
// 不受物理引擎印象(包括物理模拟,不可被唤醒)
GetComponent<Rigidbody>().isKinematic = true;
activated = false;
}
}
}
WeaponScript
实现后挂载到武器上,接着ThrowController_Clone
修改如下:
private WeaponScript weaponScript; // 武器脚本
void Start()
{
// 略......
weaponScript = weapon.GetComponent<WeaponScript>();
}
public void WeaponThrow()
{
// 激活武器脚本
weaponScript.activated = true;
// 略......
// 调整角度让斧头1字型飞出
weapon.eulerAngles = new Vector3(0, -90 +transform.eulerAngles.y, 0);
}
- 当前效果如下:
- 到这里飞出斧头的效果其实就完成了,下面再补充下这个击碎盒子的实现(这部分可跳过)
- 盒子碎开本身其实就是把正常盒子,换成碎片组成的盒子(正常盒子销毁),再施加力造成破碎效果,先看看原模型和碎片模型:
- 新建盒子破碎脚本
BreakBoxScript
:
using UnityEngine;
/// <summary>
/// 盒子破碎
/// </summary>
public class BreakBoxScript : MonoBehaviour
{
public GameObject breakedBox;
public void Break()
{
GameObject breaked = Instantiate(breakedBox, transform.position, transform.rotation);
Rigidbody[] rbs = breaked.GetComponentsInChildren<Rigidbody>();
foreach(Rigidbody rb in rbs)
{
// 每个碎片都会以自身为中心,对周围产生“爆炸”的力
rb.AddExplosionForce(150, transform.position, 30);
}
Destroy(gameObject);
}
}
- 在武器碰到盒子时,触发破碎,修改
WeaponScript
如下:
private void OnTriggerEnter(Collider other)
{
// 如果撞到可破碎盒子(盒子Tag设置为“Breakable”)
if (other.CompareTag("Breakable"))
{
if (other.GetComponent<BreakBoxScript>() != null)
{
other.GetComponent<BreakBoxScript>().Break();
}
}
}
- OK,盒子破碎就到此完成了,据说还有的项目是用动画烘焙 or 《合金装备崛起复仇》那样真正计算切痕的,这些等以后有机会再研究下。
收回斧头
- OK,扑了这么多路总算是到最关键的部分了
- 为了让斧头以曲线飞回手里,我们需要用到贝塞尔曲线,想看公式原理的可以参考知乎上的一篇文章
- 简而言之,就是定义三个点,起点+终点+控制点,再套入二阶贝塞尔的公式,就可以规划出一条路径(在路径上哪个点由t决定,0 < t < 1),二阶贝塞尔生成的曲线效果如下:
- 公式如下:
- 你也可以在shadertoy中贴入如下代码体验(shadertoy我其实还不太会用,拿别人代码改的):
float Circle(vec2 pixel, vec2 center, float radius)
{
float d = length(pixel - center);
return smoothstep(fwidth(d), .0, d - radius);
}
float LineSegment(vec2 pixel, vec2 a, vec2 b, float width)
{
vec2 a2p = pixel - a, a2b = b - a;
float t = clamp(dot(a2p, a2b) / dot(a2b, a2b), .0, 1.0);
vec2 r = a + a2b * t;
float d = length(r - pixel);
return smoothstep(fwidth(d), 0., d - width);
}
vec2 Bezier1(vec2 start, vec2 controlPtr, float timeStep)
{
return mix(start, controlPtr, timeStep);
}
vec2 Bezier2(vec2 P1, vec2 P2, vec2 P3, float timeStep)
{
return mix(Bezier1(P1, P2, timeStep), Bezier1(P2, P3, timeStep), timeStep);
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord-iResolution.xy * .5)/iResolution.x;
vec3 col = vec3(0);
float radius = .015;
float width = .001;
float t = sin(iTime) * .5 + 0.5;
vec2
Ptr1 = vec2(-.4, .2),
Ptr2 = (iMouse.xy - iResolution.xy * .5) / iResolution.x,
Ptr3 = vec2(-.4, -.2),
Ptr12 = Bezier1(Ptr1, Ptr2, t),
Ptr23 = Bezier1(Ptr2, Ptr3, t),
Ptr123 = mix(Ptr12, Ptr23, t);
col += vec3(1, 0, 0) * Circle(uv, Ptr1, radius);
col += vec3(0, 1, 0) * Circle(uv, Ptr2, radius);
col += vec3(0, 0, 1) * Circle(uv, Ptr3, radius);
col += vec3(1, 1, 0) * Circle(uv, Ptr12, radius);
col += vec3(0, 1, 1) * Circle(uv, Ptr23, radius);
col += vec3(1, 1, 1) * Circle(uv, Ptr123, radius);
col += LineSegment(uv, Ptr1, Ptr2, width);
col += LineSegment(uv, Ptr2, Ptr3, width);
col += LineSegment(uv, Ptr12, Ptr23, width);
int NUM_SEGs = 100;
vec2 currentPtr, previousPtr = Ptr1;
for (int i=1; i < NUM_SEGs+1; ++i)
{
t = float(i) / float(NUM_SEGs);
currentPtr = Bezier2(Ptr1, Ptr2, Ptr3, t);
col = max(col, LineSegment(uv, currentPtr, previousPtr, width));
previousPtr = currentPtr;
}
// Output to screen
fragColor = vec4(col, 1.0);
}
- 效果如下:
- 而我们在Unity中要复刻这种效果,同样也需要三个点,如下图所示:
- 剩下的就是把公式套到代码里面去了,核心函数就是
GetQuadraticCurvePoint
,ThrowController_Clone
整体修改如下:
private Vector3 pullPosition; // 斧头在墙上的位置(贝塞尔起点)
private float returnTime; // 飞斧返回时间,相当于贝塞尔公式的t
private Vector3 origLocPos; // 武器相对于手的位置坐标(用于复位)
private Vector3 origLocRot; // 武器相对于手的旋转角度(用于复位)
public bool pulling = false; // 是否在收回武器
public Transform curvePoint; // 贝塞尔曲线控制点
public ParticleSystem catchParticle; // 抓住武器时的粒子特效
public CinemachineImpulseSource impulseSource; // 控制摄像机震动
void Start()
{
// 略......
// 武器在手上的相对位置不会变化,保存该值以便于复位
origLocPos = weapon.localPosition;
origLocRot = weapon.localEulerAngles;
}
void Update()
{
// 略......
animator.SetBool("pulling", pulling);
// 略......
if (hasWeapon){ // 之前实现过的部分,略...... }
else
{
// 没有斧头状态下左键则收回斧头
if (Input.GetMouseButtonDown(0))
{
WeaponStartPull();
}
}
// 收回斧头中
if (pulling)
{
if(returnTime < 1)
{
weapon.position = GetQuadraticCurvePoint(returnTime, pullPosition, curvePoint.position, hand.position);
returnTime += Time.deltaTime * 1.5f;
}
else
{
WeaponCatch();
}
}
}
/// <summary>
/// 收回斧头
/// </summary>
public void WeaponStartPull()
{
// 主要是设置状态+旋转斧头
pullPosition = weapon.position;
weaponRb.Sleep();
weaponRb.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
weaponRb.isKinematic = true;
weapon.DORotate(new Vector3(-90, -90, 0), .2f).SetEase(Ease.InOutSine);
weapon.DOBlendableLocalRotateBy(Vector3.right * 90, .5f);
weaponScript.activated = true;
pulling = true;
}
/// <summary>
/// 根据起点+终点+控制点+时间计算二阶贝塞尔曲线上当前位置点
/// </summary>
/// <param name="t"></param>
/// <param name="p0">起点</param>
/// <param name="p1">控制点,也就是curvePoint</param>
/// <param name="p2">中点</param>
public Vector3 GetQuadraticCurvePoint(float t, Vector3 p0, Vector3 p1, Vector3 p2)
{
float u = 1 - t;
float tt = t * t;
float uu = u * u;
return (uu * p0) + (2 * u * t * p1) + (tt * p2);
}
/// <summary>
/// 武器重新附着在手上
/// </summary>
public void WeaponCatch()
{
returnTime = 0;
pulling = false;
weapon.parent = hand;
weaponScript.activated = false;
weapon.localEulerAngles = origLocRot;
weapon.localPosition = origLocPos;
hasWeapon = true;
//Particle and trail
catchParticle.Play();
trailRenderer.emitting = false;
trailParticle.Stop();
// 摄像机震动
impulseSource.GenerateImpulse(Vector3.right);
}
- 斧头来回的时候速度放慢些,最终效果如下: