
Shader "Game/2D/TreeHitRing_Unlit"
{
Properties
{
[PerRendererData]_MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_HitStrength ("Hit Strength", Range(0,1)) = 0
_HitPos ("Hit Pos (UV)", Vector) = (0.5,0.5,0,0)
_HitT ("Hit T (0..1)", Range(0,1)) = 0
_RingMaxRadius ("Ring Max Radius", Range(0,1)) = 0.55
_RingWidth ("Ring Width", Range(0.001,0.3)) = 0.06
_Flash ("Flash", Range(0,1)) = 0.25
}
SubShader
{
Tags
{
"Queue"="Transparent"
"RenderType"="Transparent"
"IgnoreProjector"="True"
"RenderPipeline"="UniversalPipeline"
}
Cull Off
ZWrite Off
// Blend One OneMinusSrcAlpha
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
};
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
float4 _MainTex_ST;
float4 _Color;
float _HitStrength;
float4 _HitPos;
float _HitT;
float _RingMaxRadius;
float _RingWidth;
float _Flash;
Varyings vert (Attributes v)
{
Varyings o;
o.positionHCS = TransformObjectToHClip(v.positionOS.xyz);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.color = v.color * _Color;
return o;
}
half4 frag (Varyings i) : SV_Target
{
half4 baseCol = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv) * i.color;
// 透明像素直接返回,避免边缘发光
if (baseCol.a <= 0.001) return baseCol;
float2 p = i.uv - _HitPos.xy;
float dist = length(p);
// 扩散环半径:0 -> _RingMaxRadius
float r = saturate(_HitT) * _RingMaxRadius;
// 环:基于 abs(dist - r) 的高斯/软边
float x = (dist - r) / max(_RingWidth, 1e-5);
float ring = exp(-x * x); // 1 在环上,离开快速衰减
// 闪白:前段更强,后段衰减
float flash = (1.0 - saturate(_HitT)) * _Flash;
float hit = _HitStrength * (ring * 0.85 + flash);
hit *= baseCol.a;
// 轻微偏白(你也可以改成偏黄/偏绿)
baseCol.rgb = saturate(baseCol.rgb + hit);
return baseCol;
}
ENDHLSL
}
}
}
控制脚本
using System.Collections;
using UnityEngine;
[DisallowMultipleComponent]
public class TreeHitFx : MonoBehaviour
{
static readonly int ID_HitStrength = Shader.PropertyToID( "_HitStrength" );
static readonly int ID_HitPos = Shader.PropertyToID( "_HitPos" );
static readonly int ID_HitT = Shader.PropertyToID( "_HitT" );
[Header( "Timing" )]
[SerializeField] float duration = 0.18f;
[Header( "Strength" )]
[Range( 0f, 1f )]
[SerializeField] float strength = 1.0f;
SpriteRenderer sr;
MaterialPropertyBlock mpb;
Coroutine co;
public void Init( SpriteRenderer icon )
{
sr = icon;
mpb = new MaterialPropertyBlock( );
// 初始化确保没残留
sr.GetPropertyBlock( mpb );
mpb.SetFloat( ID_HitStrength, 0f );
mpb.SetFloat( ID_HitT, 0f );
sr.SetPropertyBlock( mpb );
}
/// <summary>
/// worldHitPos:砍中点(世界坐标)
/// </summary>
public void PlayHit( Vector2 worldHitPos )
{
if ( !sr || !sr.sprite ) return;
// 计算命中点 UV(基于 sprite bounds 的近似;对大多数“树”足够用)
Vector2 uv = WorldToSpriteUV( worldHitPos );
sr.GetPropertyBlock( mpb );
mpb.SetVector( ID_HitPos, new Vector4( uv.x, uv.y, 0f, 0f ) );
mpb.SetFloat( ID_HitStrength, strength );
sr.SetPropertyBlock( mpb );
if ( co != null ) StopCoroutine( co );
co = StartCoroutine( CoHitAnim( ) );
}
IEnumerator CoHitAnim( )
{
float t = 0f;
while ( t < duration )
{
t += Time.deltaTime;
float k = Mathf.Clamp01( t / duration );
// 你可以换成 AnimationCurve,这里先给一个“先快后慢”的手感
float hitT = 1f - Mathf.Pow( 1f - k, 2f );
sr.GetPropertyBlock( mpb );
mpb.SetFloat( ID_HitT, hitT );
sr.SetPropertyBlock( mpb );
yield return null;
}
// 收尾清零,避免停留在某个状态
sr.GetPropertyBlock( mpb );
mpb.SetFloat( ID_HitStrength, 0f );
mpb.SetFloat( ID_HitT, 0f );
sr.SetPropertyBlock( mpb );
co = null;
}
Vector2 WorldToSpriteUV( Vector2 worldPos )
{
// 转本地
Vector2 local = transform.InverseTransformPoint( worldPos );
// sprite.bounds 是本地单位下的 AABB(包含 pivot 影响)
Bounds b = sr.sprite.bounds;
float u = Mathf.InverseLerp( b.min.x, b.max.x, local.x );
float v = Mathf.InverseLerp( b.min.y, b.max.y, local.y );
return new Vector2( u, v );
}
}
