Unity Day4 添加准心 制作事件系统

Unity Day4 添加准心 制作事件系统

昨天结束之后还做了一个小特性,在一次斩击未能击杀敌人时会自身会被弹开一段距离,比较简单就不写了

添加准心

由于游玩过程中玩家视线大部分时间都是在准心上的,因此准心需要具有体现部分基本信息的功能,比如:玩家速度,冲刺计量表,斩击冷却,(副武器的弹药数)等。首先把列出的前三个功能做出来。

基础准心

image
找一个四角准心的素材,初步的设想是玩家速度变快距离越高,攻击时扩张一段距离,然后随斩击冷却收缩。
先把素材导入unity,spriteMode设置为multiple,然后把四个角分别分出来。
image
ui canvas里面新建四个image分别代表四块,设置好初始位置之后开始编写准心的控制脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DynamicCrossHair : MonoBehaviour
{
    public Transform UpRight;
    public Transform UpLeft;
    public Transform DownLeft;
    public Transform DownRight;
    private Material[] _lineMaterials;
    public Player player;
    public float baseInt;
    public float dur;
    public float dul;
    public float ddr;
    public float ddl;
    [Header("settings")]
    public float SlashForce = 10f;


    private void Awake()
    {
        UpRight = transform.Find("UpRight");
        UpLeft = transform.Find("UpLeft");
        DownLeft = transform.Find("DownLeft");
        DownRight = transform.Find("DownRight");
    }

    // Start is called before the first frame update
    void Start()
    {
        Cursor.visible = false;
        player = Player.MainPlayer;
    }

    // Update is called once per frame
    void Update()
    {
        RectTransform canvasRectTransform = GetComponentInParent<Canvas>().GetComponent<RectTransform>();
        transform.position = (Vector2)Input.mousePosition;
        UpdateBaseDis();
        UpdateSelfDis();
        UpdateLine(UpRight, Vector2.up + Vector2.right, dur);
        UpdateLine(UpLeft, Vector2.up + Vector2.left, dul);
        UpdateLine(DownLeft, Vector2.down + Vector2.left, ddl);
        UpdateLine(DownRight, Vector2.down + Vector2.right, ddr);
    }
    void UpdateBaseDis()
    {
        float slashp = player.slash.getSlashColdDownProgress();
        if (slashp < 1.0f)
        {
            ddl = ddr = dul = dur = 12f;
        }
        else
        {
            float v = player.rb.velocity.magnitude;
            float x = (1.0f + 0.8f * Mathf.Min(1f, Mathf.Log(v + 1f, 30))) * 16f;
            ddl = ddr = dul = dur = x;
            baseInt = x;
        }
    }
    void UpdateSelfDis()
    {
        float slashp = player.slash.getSlashColdDownProgress();
        if (slashp < 1.0f)
        {
            ddl += SlashForce;
            ddr += SlashForce;
            dul += SlashForce;
            dur += SlashForce;
            if (slashp > 0.2f) dur -= SlashForce;
            if (slashp > 0.4f) ddr -= SlashForce;
            if (slashp > 0.6f) ddl -= SlashForce;
            if (slashp > 0.8f) dul -= SlashForce;
            
        }

    }
    void UpdateLine(Transform trans,Vector2 dir,float tar)
    {
        float current =Mathf.Abs( trans.localPosition.x);
        float next = Mathf.Lerp(current, tar, Time.deltaTime * 50f);
        trans.localPosition = new Vector2(next, next) *dir;
    }
}

每帧动态计算准星应该所处的位置,然后平滑调整。
接下来实现准心颜色的变化。考虑变化的步骤:底色为灰色,恢复时沿着对角线一层一层逐渐铺黄色,蓝色,白色。因此新建Shader:

Shader "Custom/AnimatedCrosshairLine"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _BaseColor ("Base Color", Color) = (0.5, 0.5, 0.5, 1)   
        _YellowLayer ("Yellow Layer", Color) = (1, 0.8, 0, 1)   
        _BlueLayer ("Blue Layer", Color) = (0, 0.5, 1, 1)      
        _FinalColor ("Final Color", Color) = (1, 1, 1, 1)       
        
        _AnimationSpeed ("Animation Speed", Range(0.1, 5)) = 1
        _PhaseOffset ("Phase Offset", Range(0, 1)) = 0
        _PulseFrequency ("Pulse Frequency", Range(0, 10)) = 2
        
        _WaveAmplitude ("Wave Amplitude", Range(0, 0.5)) = 0.1
        _WaveSpeed ("Wave Speed", Range(0, 5)) = 1
        
        _GlowIntensity ("Glow Intensity", Range(0, 5)) = 1
        _Progress ("Animation Progress", Range(0, 1)) = 0

        _DirectionX("Animation Direction X",float)= -1
        _DirectionY("Animation Direction Y",float)= -1

        _StartX("Animation Start X",float)= 0
        _StartY("Animation Start Y",float)= 1
    }
    
    SubShader
    {
        Tags 
        { 
            "Queue"="Transparent" 
            "IgnoreProjector"="True" 
            "RenderType"="Transparent" 
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }
        
        Cull Off
        Lighting Off
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ PIXELSNAP_ON
            #include "UnityCG.cginc"
            
            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                float2 worldPos : TEXCOORD1;
            };
            
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _BaseColor;
            fixed4 _YellowLayer;
            fixed4 _BlueLayer;
            fixed4 _FinalColor;
            float _AnimationSpeed;
            float _PhaseOffset;
            float _PulseFrequency;
            float _WaveAmplitude;
            float _WaveSpeed;
            float _GlowIntensity;
            float _Progress;
            float _StartX;
            float _StartY;
            float _DirectionX;
            float _DirectionY;
            
            v2f vert(appdata_t IN)
            {
                v2f OUT;
                
                float wave = sin(_Time.y * _WaveSpeed + IN.texcoord.x * 20) * _WaveAmplitude;
                IN.vertex.y += wave;
                
                OUT.vertex = UnityObjectToClipPos(IN.vertex);
                OUT.texcoord = TRANSFORM_TEX(IN.texcoord, _MainTex);
                OUT.color = IN.color;
                OUT.worldPos = mul(unity_ObjectToWorld, IN.vertex).xy;
                
                #ifdef PIXELSNAP_ON
                OUT.vertex = UnityPixelSnap(OUT.vertex);
                #endif
                
                return OUT;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 tex = tex2D(_MainTex, IN.texcoord);
                
                float time = _Time.y * _AnimationSpeed;
                float pulse = sin(time * _PulseFrequency + _PhaseOffset * 6.28) * 0.5 + 0.5;
                
                float progress = saturate(_Progress);

                float yellowWeight = saturate(progress * 4.0 - 1) ;
                float blueWeight = saturate(progress * 4.0 - 2);
                float finalWeight = saturate(progress * 4.0 - 3);
                fixed4 color = _BaseColor * 1;
                float2 fixedstart=float2(_StartX,_StartY);
                float2 fixedPos=IN.texcoord-fixedstart;
                float ps=fixedPos*float2(_DirectionX,_DirectionY);
                if(yellowWeight>=ps)color=_YellowLayer;
                if(blueWeight>=ps)color=_BlueLayer;
                if(finalWeight>=ps)color=_FinalColor;
                float glow = saturate(blueWeight * 0.8 + finalWeight) * _GlowIntensity;
                color.rgb += glow * fixed3(0.8, 0.9, 1.0);
                color.a *= tex.a * IN.color.a;
                float edge = smoothstep(0.1, 0.9, IN.texcoord.x);
                color.rgb *= 1.0 + (1.0 - edge) * 0.3 * pulse;
                
                return color;
            }
            ENDCG
        }
    }
}

通过设置Start,Direction来设置变换的方向和起点,设置progress来设置恢复进度。
接下来在控制器里加入材质的动态更改:

private void Awake()
{
    UpRight = transform.Find("UpRight");
    UpLeft = transform.Find("UpLeft");
    DownLeft = transform.Find("DownLeft");
    DownRight = transform.Find("DownRight");
    _lineMaterials = new Material[4];
    _lineMaterials[0]=new Material(UpRight.GetComponent<Image>().material);
    UpRight.GetComponent<Image>().material = _lineMaterials[0];
    _lineMaterials[1]=new Material(DownRight.GetComponent<Image>().material);
    DownRight.GetComponent<Image>().material = _lineMaterials[1];
    _lineMaterials[2]=new Material(DownLeft.GetComponent<Image>().material);
    DownLeft.GetComponent<Image>().material = _lineMaterials[2];
    _lineMaterials[3]=new Material(UpLeft.GetComponent<Image>().material);
    UpLeft.GetComponent<Image>().material = _lineMaterials[3];
    for (int i = 0; i < 4; i++)
    {
        _lineMaterials[i].SetFloat("_Progress", 1f);
    }
    setMatAnimData(_lineMaterials[0], new Vector2(-1, -1), new Vector2(1, 0));
    setMatAnimData(_lineMaterials[1], new Vector2(-1, 1), new Vector2(1, 1));
    setMatAnimData(_lineMaterials[2], new Vector2(1, 1), new Vector2(0, 1));
    setMatAnimData(_lineMaterials[3], new Vector2(1, -1), new Vector2(0, 0));
}
private void setMatAnimData(Material mat,Vector2 dir,Vector2 start)
{
    mat.SetFloat("_StartX", start.x);
    mat.SetFloat("_StartY", start.y);
    mat.SetFloat("_DirectionX", dir.x);
    mat.SetFloat("_DirectionY", dir.y);
}
void UpdateSelfDis()
{
    float slashp = player.slash.getSlashColdDownProgress();
    if (slashp < 1.0f)
    {
        ddl += SlashForce;
        ddr += SlashForce;
        dul += SlashForce;
        dur += SlashForce;
        if (slashp > 0.2f) dur -= SlashForce;
        if (slashp > 0.4f) ddr -= SlashForce;
        if (slashp > 0.6f) ddl -= SlashForce;
        if (slashp > 0.8f) dul -= SlashForce;
        
    }
    for (int i = 0; i < 4; i++)
    {
        float p = Mathf.Clamp(slashp * 5f - 1.0f * i, 0, 1);
        _lineMaterials[i].SetFloat("_Progress", p);
    }
}

注意对于每个物体上都要新建一个材质并替换,不然操作的就是整体的同一个材质。
运行游戏,准心表现很好,很有动态感。

给准心添加更多功能

计划在准心周围添加冲刺的计量表。设计一个圆弧形的条,切割成多个小段,根据恢复时间实时渲染每一段。
感觉纯用Image的功能去渲染的话不太好写,干脆纯用Shader去处理吧。

Shader "Custom/DynamicDashTab"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _EmptyColor("Empty Color",Color)=(1,1,1,0)
        _BaseColor ("Base Color", Color) = (0.3, 0.3, 0.3, 0.5)   
        _ChargeLayer ("Charge Layer", Color) = (1.0, 0.713, 0.713, 1)   
        _FullLayer ("Full Layer", Color) = (0.678, 0.756, 901, 1)      
        _MaxDashCount("Max Dash Count",Int)=2
        _Gap("Gap size",range(0,0.1))=0.02
        _StartAngle ("Start Angle", Range(0,360)) = 320
        _ArcAngle ("Arc Angle", Range(0,360)) = 80
        _ArcR("Arc Radius",range(0,1))=0.3
        _ArcW("Arc Width",range(0,1))=0.1

        _DashCount("Now Dash Count",Int)=1
        _DashRecProgress("Dash Recover Progress",range(0,1))=0.5

        _LastDashRec("_LastDashRec",Int)=1
        _RecAnimStartR("_RecAnimStartR",Float)=0.2
        _RecAnimR("_RecAnimR",Float)=0.7
        _LastRecAnimProgress("_LastRecAnimProgress",range(0,1))=0.5

    }
    SubShader
    {

        Tags { "Queue" = "Transparent" "RenderType" = "Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _BaseColor;
            float4 _ChargeLayer;
            float4 _FullLayer;
            float4 _EmptyColor;
            int _MaxDashCount;
            float _Gap;
            float _StartAngle;
            float _ArcAngle;
            float _ArcR;
            float _ArcW;

            int _DashCount;
            float _DashRecProgress;
            float _RecAnimStartR;
            float _RecAnimR;
            int _LastDashRec;
            float _LastRecAnimProgress;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }


            fixed4 frag (v2f i) : SV_Target
            {
                //fixed4 col = tex2D(_MainTex, i.uv);
                // just invert the colors
                //col.rgb = 1 - col.rgb;
                float2 center = float2(0.5, 0.5);
                float2 dir = i.uv - center;
                 float angle = atan2(dir.y, dir.x) * 57.2958; 
                angle = fmod(angle + 360 - _StartAngle, 360);
                float radius = length(dir);
                if (angle > _ArcAngle ){
                    discard;
                }
                // float4 layer1=_EmptyColor;

                 fixed4 col = _BaseColor;
                 
                float segmentAngle = _ArcAngle / _MaxDashCount;
                int segment=angle/segmentAngle;
                float segmentPos = fmod(angle, segmentAngle) / segmentAngle;
                
                if(segment<_DashCount){
                    col=_FullLayer;
                }
                else if(segment==_DashCount){
                    if(segmentPos<=_DashRecProgress){
                        col=_FullLayer;
                     }
                }else{
                    col=_BaseColor;
                 }
                 if((segmentPos < _Gap || segmentPos > 1 - _Gap)){
                     col=_BaseColor;
                     }
                 if(radius < _ArcR-_ArcW/2|| radius >_ArcR+_ArcW/2){
                    if(radius<_RecAnimStartR||radius>_RecAnimR)discard;
                    col=_BaseColor;
                  }

                 if(_LastRecAnimProgress!=-1){
                            float p=_LastRecAnimProgress;
                            float l=_RecAnimR-_RecAnimStartR;
                            if(radius>=_RecAnimStartR){
                                float maxc=1.0-saturate(p-0.5f)/0.5f;
                                float nmaxr=l*p;
                                if(radius<=nmaxr+_RecAnimStartR){
                                    float s=(radius-_RecAnimStartR);
                                    col=lerp(col,_ChargeLayer,saturate(s/nmaxr+0.3f)*maxc);
                             }
                        }
                 }
                return col;
            }
            ENDCG
        }
    }
}

写的时候遇到一个严重的问题,ui的透明度不生效。改来改去发现Shader没有启用透明度混合,无语了。开上就好了:

 Tags { "Queue" = "Transparent" "RenderType" = "Transparent" }
 Blend SrcAlpha OneMinusSrcAlpha

image

事件系统

接下来开始对事件系统的搭建。这个系统将搭建大多数实体之间交互的桥梁,方便开发和维护。包括后续的拾取武器,开火和之前做好的斩击伤害都会重构为以事件系统为底子的结构。

结构设计

简单来说,最基础的事件系统需要一个类来进行事件的注册和分发,最后返回结果。例如攻击事件。首先对攻击事件进行注册,然后玩家在斩击判定时广播一个玩家攻击实体的事件,最后根据这个事件在广播后收到的结果来判断伤害,或者是令这次攻击无效化之类的。广播者会把这个事件依次发送到所有订阅了这个时间的类中,让他们处理这个事件。
着手搭建事件管理器:
事件总线:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EventBus 
{
    public static Dictionary<string,List<Subscriber>> Subsribers= new Dictionary<string,List<Subscriber>>();
    public static List<EventBase> Events = new List<EventBase>();
    public static List<string> EventIds = new List<string>();
    public static Logger logger = new Logger(new MyLogHandler());

    public static bool inited = false;
    public static void Init()
    {
        if (inited) return;
        inited = true;
        registerEvent(new EntityDamagedEvent());
        registerEvent(new EntityDamagedByEntityEvent());
    }
    public static void Log(string s)
    {
        logger.Log("[EVENT BUS]", s);
    }
    public static void registerEvent(EventBase e) {
        if (EventIds.Contains(e.id))
        {
            Log("事件 " + e.id + " 已被注册");
        }
        Events.Add(e);
        EventIds.Add(e.id);
    }
    public static void registerSubscriber<T>(Subscriber subscriber)
    {
        foreach (EventBase i in Events)
        {
            if (i is T)
            {
                List<Subscriber> list = Subsribers[i.id];
                if (list == null)
                {
                    list = new List<Subscriber>();
                    Subsribers[i.id] = list;
                }
                list.Add(subscriber);
            }
        }
    }
    public static void BordercastEvent(EventBase e)
    {
        if (!Events.Contains(e))
        {
            Log("事件 " + e.id + " 未注册");
        }
        if (Subsribers.ContainsKey(e.id))
        {
            foreach (Subscriber i in Subsribers[e.id])
            {
                i.OnEvent(e);
            }

        }
    }
}

事件基类:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class EventBase
{
    public string id;
    public bool Cancellable = false;
    public bool isCancelled = false;
    public EventBase(string id,bool cancellable)
    {
        this.id = id;
        this.Cancellable = cancellable;
    }
    public void SetCancelled(bool cancellable)
    {
        Cancellable = cancellable;
    }
}

订阅器:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class Subscriber 
{
    public Subscriber()
    {

    }
    public virtual void OnEvent(EventBase e)
    {

    }
}

接下来完成EntityEvent,EntityDamagedEvent和EntityDamagedByEntityEvent:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class EntityEvent : EventBase
{
    public EntityBase Entity;
    public EntityEvent(string id, bool cancellable) : base(id, cancellable)
    {
    }
    public EntityEvent(EntityBase entity):base("EntityEvent",true)
    {
        Entity = entity;
    }

}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EntityDamagedEvent : EntityEvent
{
    public float Damage;
    public EntityDamagedEvent() : base("EntityDamagedEvent", true)
    {

    }
    public EntityDamagedEvent(EntityBase entity) : this()
    {
        this.Entity= entity; 

    }
    public EntityDamagedEvent(EntityBase entity,float damage) : this(entity)
    {
        this.Damage = damage;
    }

    public EntityDamagedEvent(string id, bool cancellable) : base(id, cancellable)
    {
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EntityDamagedByEntityEvent : EntityDamagedEvent
{
    public EntityBase Damager;
    public EntityDamagedByEntityEvent(): base("EntityDamagedByEntityEvent", true)
    {

    }
    public EntityDamagedByEntityEvent(EntityBase entity) : this()
    {
        this.Entity = entity;
    }
    public EntityDamagedByEntityEvent(EntityBase entity,EntityBase damager,float damage) : this(entity)
    {
        this.Damage = damage;
        this.Damager = damager;
    }
}

好像要被C#搞得大脑升级了……
然后来到SlashControl,把攻击步骤连上事件系统:

 if(collision.GetComponent<EntityBase>() != null)
 {
     EntityBase entity= collision.GetComponent<EntityBase>();
     EntityDamagedByEntityEvent e = new EntityDamagedByEntityEvent(entity, player, SlashDamage);
     EventBus.BordercastEvent(e);
     if (e.isCancelled) return;
     entity.Damage(e.Damage);
     AttackResult= entity.isDead;
 }

运行游戏,攻击系统依然正常。
不知道有没有空,总之继续往下做

设计远程武器

说到初始远程武器那必须得是这个民用加特林磁轨炮啊,劲道足口味正,我们加达里公民就好这一口
思考整体的武器系统运作:
首先要有一个锁定系统,根据武器不同锁定速度也不同。准心一定范围内自动开始锁定,开火时自瞄距离准心最近的被锁定单位,如果没有就不自瞄。对被锁定单位有更高伤害和暴击率。例外:导弹。导弹可以锁定目标多次,锁定次数决定朝着该目标射击多少发导弹,且攻击时会解除所有锁定。没有锁定目标时相当于全自动射击无制导飞弹。
思考整体武器系统的结构(由于武器肯定要涉及到物品模块,先只设计已经被装备的武器):

  • 武器本身:
    最大弹药,射击速率,射击模式,速度修改,后坐力,是否能奔跑使用,射击散布,弹药本身散布,单次射击弹药量,是否抛壳
  • 弹药种类:
    伤害,类型(射弹,激光,导弹),弹速,弹药大小,弹药最大存在时长
  • 渲染相关
    武器材质,弹壳材质(如果有),武器动画组(如果有)

有点晚了,先这样吧。明天一起把物品+武器做好

posted @ 2025-06-11 21:51  国土战略局特工  阅读(41)  评论(0)    收藏  举报