Unity Day4 添加准心 制作事件系统
Unity Day4 添加准心 制作事件系统
昨天结束之后还做了一个小特性,在一次斩击未能击杀敌人时会自身会被弹开一段距离,比较简单就不写了
添加准心
由于游玩过程中玩家视线大部分时间都是在准心上的,因此准心需要具有体现部分基本信息的功能,比如:玩家速度,冲刺计量表,斩击冷却,(副武器的弹药数)等。首先把列出的前三个功能做出来。
基础准心

找一个四角准心的素材,初步的设想是玩家速度变快距离越高,攻击时扩张一段距离,然后随斩击冷却收缩。
先把素材导入unity,spriteMode设置为multiple,然后把四个角分别分出来。

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

事件系统
接下来开始对事件系统的搭建。这个系统将搭建大多数实体之间交互的桥梁,方便开发和维护。包括后续的拾取武器,开火和之前做好的斩击伤害都会重构为以事件系统为底子的结构。
结构设计
简单来说,最基础的事件系统需要一个类来进行事件的注册和分发,最后返回结果。例如攻击事件。首先对攻击事件进行注册,然后玩家在斩击判定时广播一个玩家攻击实体的事件,最后根据这个事件在广播后收到的结果来判断伤害,或者是令这次攻击无效化之类的。广播者会把这个事件依次发送到所有订阅了这个时间的类中,让他们处理这个事件。
着手搭建事件管理器:
事件总线:
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;
}
运行游戏,攻击系统依然正常。
不知道有没有空,总之继续往下做
设计远程武器
说到初始远程武器那必须得是这个民用加特林磁轨炮啊,劲道足口味正,我们加达里公民就好这一口
思考整体的武器系统运作:
首先要有一个锁定系统,根据武器不同锁定速度也不同。准心一定范围内自动开始锁定,开火时自瞄距离准心最近的被锁定单位,如果没有就不自瞄。对被锁定单位有更高伤害和暴击率。例外:导弹。导弹可以锁定目标多次,锁定次数决定朝着该目标射击多少发导弹,且攻击时会解除所有锁定。没有锁定目标时相当于全自动射击无制导飞弹。
思考整体武器系统的结构(由于武器肯定要涉及到物品模块,先只设计已经被装备的武器):
- 武器本身:
最大弹药,射击速率,射击模式,速度修改,后坐力,是否能奔跑使用,射击散布,弹药本身散布,单次射击弹药量,是否抛壳 - 弹药种类:
伤害,类型(射弹,激光,导弹),弹速,弹药大小,弹药最大存在时长 - 渲染相关
武器材质,弹壳材质(如果有),武器动画组(如果有)
有点晚了,先这样吧。明天一起把物品+武器做好

浙公网安备 33010602011771号