【Unity】动作游戏开发实战详细分析-24-流血喷溅程序
溅落血迹效果
实现思路
利用对象池的代码设计思路,通过随机性来实现随机的溅落血迹效果。
代码
public class DripBloodFx_Full : MonoBehaviour
{
public GameObject[] templates;
public float depthOffset = 0.001f;
public float directionRandomRange = 30f;
public Vector2 decalDelay = new Vector2(0.1f, 0.15f);
public LayerMask layerMask;
public int poolCount = 10;
public float delay;
GameObject[] mPool;
WaitForSeconds mCacheDelayWaitForSeconds;
void Awake()
{
mCacheDelayWaitForSeconds = new WaitForSeconds(delay);
mPool = new GameObject[templates.Length * poolCount];
for (int i = 0, k = 0; i < templates.Length; i++)
{
for (int j = 0; j < poolCount; j++)
{
var instanced = Instantiate(templates[i]);
instanced.name = "decal_" + i + "_" + j;
instanced.gameObject.SetActive(false);
mPool[k] = instanced;
k++;
}
}
}
void OnEnable()
{
StartCoroutine(TriggerDripBlood());
}
IEnumerator TriggerDripBlood()
{
yield return mCacheDelayWaitForSeconds;
var templateIndex = UnityEngine.Random.Range(0, templates.Length);
var targetPoolItem = default(GameObject);
for (int i = 0; i < poolCount; i++)
{
var item = mPool[templateIndex * poolCount + i];
if (!item.activeSelf)
{
targetPoolItem = item;
break;
}
}
if (targetPoolItem == null)
{
targetPoolItem = mPool[templateIndex * poolCount];
for (int i = 0; i < poolCount - 1; i++)
mPool[i] = mPool[i + 1];
mPool[templateIndex * poolCount + (poolCount - 1)] = targetPoolItem;
}
var bloodDecal = targetPoolItem;
var dripDirection = ConeRandom(transform.forward, directionRandomRange);
var raycastHit = default(RaycastHit);
var isHit = Physics.Raycast(new Ray(transform.position, dripDirection), out raycastHit, layerMask);
if (isHit)
{
bloodDecal.gameObject.SetActive(true);
bloodDecal.transform.position = raycastHit.point + raycastHit.normal * depthOffset;
bloodDecal.transform.forward = raycastHit.normal;
}
}
Vector3 ConeRandom(Vector3 direction, float range)
{
var quat = Quaternion.FromToRotation(Vector3.forward, direction);//构建forward四元数
var upAxis = quat * Vector3.up;
var rightAxis = quat * Vector3.right;
//并以此得到另外两个轴
var quat1 = Quaternion.AngleAxis(UnityEngine.Random.Range(-range * 0.5f, range), upAxis);
var quat2 = Quaternion.AngleAxis(UnityEngine.Random.Range(-range * 0.5f, range), rightAxis);
var r = quat1 * quat2 * direction;//通过横向与纵向随机得到两个四元数并与默认方向相乘,得到随机偏移结果
return r;
}
void OnDrawGizmos()
{
Gizmos.DrawWireSphere(transform.position, 0.05f);
Gizmos.DrawLine(transform.position, transform.position + transform.forward);
}
}
血液飞溅效果
代码实现
拖尾网格控制器
拖尾的数据存放在以下的结构体中,它存放中心点信息与垂直轴。根据中心点与垂直轴可以创建两个顶点,因此一个拖尾数据可以对应两个顶点,因此构建一个网格最少需要两个拖尾数据
public struct TrailSection
{
public Vector3 Point { get; set; }//中心点信息
public Vector3 UpAxis { get; set; }//垂直轴
public float CreateTime { get; set; }//创建时间
}
网格控制器代码如下,解析见注释
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class TrailMeshController : MonoBehaviour
{
const int MESH_STRUCT_CACHE_COUNT = 512;
const int SECTION_CACHE_COUNT = 32;
public Vector2 widthRange = new Vector2(0.1f, 0f);//网格宽度
public float durationTime = 2f;//拖尾持续时间
public Color startVertColor = Color.white;//初始顶点色
public Color endVertColor = new Color(1f, 1f, 1f, 0f);//结束顶点色
//时间重映射曲线
public AnimationCurve evaluateCurve = new AnimationCurve(new Keyframe[] { new Keyframe(0, 0), new Keyframe(1, 1) });
//网格组件
Mesh mCacheMesh;
//顶点存储列表
List<Vector3> mCacheVertexList;
//顶点色存储列表
List<Color> mCacheColorList;
//Uv存储列表
List<Vector2> mCacheUvList;
//三角形顶点顺序列表
List<int> mCacheTriangleList;
//拖尾数据
List<TrailSection> mSectionList;
void Awake()
{
mCacheVertexList = new List<Vector3>(MESH_STRUCT_CACHE_COUNT);
mCacheColorList = new List<Color>(MESH_STRUCT_CACHE_COUNT);
mCacheUvList = new List<Vector2>(MESH_STRUCT_CACHE_COUNT);
mCacheTriangleList = new List<int>(MESH_STRUCT_CACHE_COUNT);
mSectionList = new List<TrailSection>(SECTION_CACHE_COUNT);
mCacheMesh = GetComponent<MeshFilter>().mesh;
//初始化操作
}
public void Itearate(Vector3 position, Vector3 upAxis, float time)
{
var section = new TrailSection();
section.Point = position;
section.UpAxis = upAxis;
section.CreateTime = time;
mSectionList.Insert(0, section);//从列表的第一个位置添加新的拖尾数据,方便之后可以从后向前根据时间消逝剔除数据
}//进行一次迭代
public void UpdateTrail(float currentTime, float deltaTime)
{
mCacheMesh.Clear();
while (mSectionList.Count > 0 && currentTime > mSectionList[mSectionList.Count - 1].CreateTime + durationTime)
mSectionList.RemoveAt(mSectionList.Count - 1);
//判断每一个部分的时间,若其持续时间和初始时间小于当前时间,则移除
if (mSectionList.Count < 2)
return;
//若拖尾的处理部分少于2则跳出,因为无法构成一个面片
mCacheVertexList.Clear();
mCacheColorList.Clear();
mCacheUvList.Clear();
mCacheTriangleList.Clear();
//清楚之前的数据
var w2lMatrix = transform.worldToLocalMatrix;//记录网格
//参数初始化
for (int i = 0, iMax = mSectionList.Count; i < iMax; i++)
{
var item = mSectionList[i];
var delta = Mathf.Clamp01((currentTime - item.CreateTime) / durationTime);//计算持续时间内的时间片
delta = evaluateCurve.Evaluate(delta);
//更新时间并映射至曲线
var half_height = Mathf.Lerp(widthRange.x, widthRange.y, delta) * 0.5f;
var color = Color.Lerp(startVertColor, endVertColor, delta);//根据时间片插值计算顶点色
var upAxis = item.UpAxis;
//获取当前的高度,垂直轴,颜色信息
mCacheVertexList.Add(w2lMatrix.MultiplyPoint(item.Point - upAxis * half_height));
mCacheVertexList.Add(w2lMatrix.MultiplyPoint(item.Point + upAxis * half_height));
//顶点位置更新
mCacheUvList.Add(new Vector2(item.CreateTime, 0f));
mCacheUvList.Add(new Vector2(item.CreateTime, 1f));
//uv位置更新
mCacheColorList.Add(color);
mCacheColorList.Add(color);
//顶点色更新
}
var trianglesCount = (mSectionList.Count - 1) * 2 * 3;
for (int j = 0; j < trianglesCount / 6; j++)
{
mCacheTriangleList.Add(j * 2);
mCacheTriangleList.Add(j * 2 + 1);
mCacheTriangleList.Add(j * 2 + 2);
mCacheTriangleList.Add(j * 2 + 2);
mCacheTriangleList.Add(j * 2 + 1);
mCacheTriangleList.Add(j * 2 + 3);
}//顶点顺序更新
mCacheMesh.SetVertices(mCacheVertexList);
mCacheMesh.SetColors(mCacheColorList);
mCacheMesh.SetUVs(0, mCacheUvList);
mCacheMesh.SetTriangles(mCacheTriangleList, 0);
//设置到网格,这种使用List的形式不会产生GC
}
public void ClearTrail()
{
if (mCacheMesh != null)
{
mCacheMesh.Clear();
mSectionList.Clear();
}//清空拖尾
}
}
拖尾组件
public class TrailFx : MonoBehaviour
{
public TrailMeshController trailMeshController;
public int itearate = 3;//每帧叠代次数
public bool toggle = true;
Transform mCacheMainCameraTransform;//缓存主相机变换
//存储上一次的位置
Vector3? mLastPosition;
float mLastTime;
public Transform TrackPoint { get { return transform; } }//跟踪点
void OnEnable()
{
mCacheMainCameraTransform = Camera.main.transform;
}
void LateUpdate()//使用LateUpdate时序更新,以便在动画更新后执行该效果
{
if (mLastPosition != null)//若存在上一帧位置,则进入处理
{
if (toggle)//加入开关让拖尾不再触发,达到已存在拖尾的自然消解效果
{
for (int i = 1; i <= itearate; i++)
{
var delta = i / (float)itearate;//计算时间片
var pos = Vector3.Slerp(mLastPosition.Value, TrackPoint.position, delta);
//与上一帧位置进行插值
var time = Mathf.Lerp(mLastTime, Time.time, delta);
//取时间差并按照叠代数量获取当前片的时间
var forward = (mCacheMainCameraTransform.position - pos).normalized;//计算朝向
var tangent = (pos - mLastPosition.Value);
if (tangent == Vector3.zero) tangent = mCacheMainCameraTransform.right;
//向量判断有进行内部重写,故进行zero比较。若无移动量则应用相机方位
tangent = tangent.normalized;
var bionormal = Vector3.Cross(forward, tangent);
//以相机相对位置作为法线,移动方向作为切线,求得垂直轴
var up = bionormal;
trailMeshController.Itearate(pos, up, time);//更新叠代
}
}
trailMeshController.UpdateTrail(Time.time, Time.deltaTime);//更新网格
}
mLastPosition = TrackPoint.position;//缓存位置下一次更新使用
mLastTime = Time.time;//缓存时间下一次更新使用
}
public void ClearTrail()
{
mLastPosition = null;
trailMeshController.ClearTrail();
}
}
Shader
Shader "ACTBook/BloodLineFX"
{
Properties
{
_Color("Color", Color) = (1, 1, 1, 0.2)
_MainTex("MainTexture", 2D) = "white" {}
_NoiseTex("NoiseTexture", 2D) = "white" {}
_Amp("Amp", vector) = (1, 1, 1, 1)
//xyz表示不同轴的偏移值缩放,w表示受顶点色影响的程度
}
SubShader
{
CULL Off
ZWrite Off
tags
{
"Queue" = "Transparent"
"RenderType" = "Transparent"
}
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
fixed4 color : Color;
half2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
};
sampler2D _MainTex;
sampler2D _NoiseTex;
half4 _Amp;
fixed4 _Color;
#define UV_VERT_COLOR_OFFSET 0.01
v2f vert(appdata_full v)
{
v2f o = (v2f)0;
o.uv = v.texcoord;
o.color = v.color;
half3 offsetSample = tex2Dlod(_NoiseTex, float4(o.uv.x + v.color.a * UV_VERT_COLOR_OFFSET, o.uv.y, 0, 0)).rgb;//通过噪声图获得偏移值的采样结果
offsetSample = (offsetSample - 0.5) * 2;//映射:[0,1]->[-1,1]
half3 offsetForce = offsetSample * pow((1 - v.color.a), _Amp.w);//将采样结果乘以顶点色系数,这样越往后的拖尾可以得到越大的偏移效果
half3 offsetDir = half3(offsetForce.x * _Amp.x, offsetForce.y * _Amp.y, offsetForce.z * _Amp.z);//缩放偏移
o.pos = UnityObjectToClipPos(v.vertex + offsetDir);//对坐标施加偏移效果
UNITY_TRANSFER_FOG(o, o.pos);
return o;
}
fixed4 frag(v2f i) : COLOR
{
fixed4 mainTex = tex2D(_MainTex, i.uv);
fixed4 result = mainTex * _Color;
result.a *= i.color.a;
UNITY_APPLY_FOG(i.fogCoord, result);
return result;
}
ENDCG
}
}
}