【Unity】动作游戏开发实战详细分析-24-流血喷溅程序

【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
		}
	}
}

猜你喜欢

转载自blog.csdn.net/qq_52324195/article/details/125960204