一般来讲,在回合制和RPG游戏中,不可避免的需要在人物头上显示被攻击的伤害值,或则是被治疗时候的治疗值。一般就是一些跟随人物Label就可以了,加上一些出现的小动画就可以了。
一、效果元素组
//动画的一个状态元素 using UnityEngine; public class PopElement { public float posx = 0.0f;//起始位置 public float posy = 0.0f; public float detailTime = 0.0f;//这个状态的总共时间 public float transferX = 0.0f;//移动的偏移位置 public float transferY = 0.0f; public float scale = 1.0f;//缩放 public float startAlpha = 1.0f;//开始的透明度 public float alpha = 1.0f;//结束的透明度 float totalTime = 0f; public PopElement(Vector2 pos, float time, Vector2 transfer, float scale, float startalpha, float alpha) { this.posx = pos.x; this.posy = pos.y; this.detailTime = time; this.transferX = transfer.x; this.transferY = transfer.y; this.scale = scale; this.startAlpha = startalpha; this.alpha = alpha; Init(); } public void Init() { totalTime = detailTime; } public void Update(float time) { totalTime -= time; } public bool isEnd() { return totalTime <= 0; } }
//一个效果将由很多个动画状态组成 using System.Collections.Generic; public class PopEffect { public List<PopElement> elementList = new List<PopElement>(); }
//具体的一个效果 using UnityEngine; public class PopEffectHurt : PopEffect { public PopEffectHurt() { elementList.Add(new PopElement(new Vector2(0f, 0f), 0.15f, new Vector2(0f, 1.99f), 1.05f, 0.3f, 1f));//0.15秒 从透明到显示 放大一点 向上移动1.99 elementList.Add(new PopElement(new Vector2(0f, 0f), 0.15f, new Vector2(0f, 1.99f), 0.95f, 1f, 1f));//0.15秒 往上走 缩小一点 elementList.Add(new PopElement(new Vector2(0f, 0f), 0.8f, new Vector2(0f, 0f), 1.0f, 1f, 1f));//停留不动0.8秒 elementList.Add(new PopElement(new Vector2(0f, 0f), 0.1f, new Vector2(0, -0.5f), 0.95f, 1f, 0.3f));//0.1秒 往下走并隐藏 } }
二、那个Damage物体,一般是一个Label或Text
//一个PDamage代表一个伤害Label的实例 using UnityEngine; using UnityEngine.UI; public class PDamage { public GameObject go = null;//实例化出来的物体 public Text lable = null;//显示数值的Label,在go下的 public PopEffect effect = null;//使用的动画效果 public void Destory() { GameObject.Destroy(go); go = null; } public void Clear() { effect = null; go.SetActive(false); } }
//将会绑定到上面的go上的,这个是基类,一些移动缩放偏移等的基本实现 using UnityEngine; using UnityEngine.UI; public class UIHeadElement : MonoBehaviour { protected GameObject attachedObject;//联系上的物体,3D人物 protected Vector3 worldPos = Vector3.zero; protected Vector3 attachedOffset = Vector3.zero;//偏移 private bool isTransfer = false;//是否计算移动 protected float transX = 0.0f;//总的偏移量 protected float transY = 0.0f; private float shiftPosX = 0.0f;//当前的偏移移动量 private float shiftPosY = 0.0f; private float startPosX = 0.0f;//起始位置 private float startPosY = 0.0f; protected float transScale = 1.0f;//缩放因子 private float ClipFar = 100; //远剪裁面的距离,用来计算大小,实现越远数值大小也越小 Vector3 curPos = Vector3.zero; Transform trans; public Text chatLable; protected void Awake() { trans = gameObject.transform; GameObject uiRt = GameObject.Find("Canvas"); trans.parent = uiRt.transform; InitHead(); } void InitHead() { worldPos = Vector3.zero; shiftPosX = 0f; shiftPosY = 0f; trans.localPosition = Vector3.zero; trans.localRotation = Quaternion.identity; if (attachedObject != null) { trans.localScale = Vector3.one * (1 - CameraManager.Instance.CalObjDepthFromCamera( attachedObject.transform.position.z ) / ClipFar); } trans.position = new Vector3(10000.0f,10000.0f,0.0f); //先移到看不到的地方 } protected void Update () { if(attachedObject == null) return; CalcBoxPos(); ShowBoxPos(); } protected void Tick (float time) { if(attachedObject == null) return; CalcBoxPos(time); ShowBoxPos(); } void CalcBoxPos(float time) { worldPos = attachedObject.transform.position + attachedOffset; if (isTransfer) { //计算动画的移动 和 缩放 shiftPosX = shiftPosX + transX * time; shiftPosY = shiftPosY + transY * time; worldPos.x += shiftPosX; worldPos.y += shiftPosY; worldPos.x += startPosX; worldPos.y += startPosY; trans.localScale *= transScale;//直接乘于缩放因子 } worldPos = CameraManager.Instance.PosFromMainCamToUICam(worldPos); worldPos.z = trans.position.z; } void CalcBoxPos() { worldPos = attachedObject.transform.position + attachedOffset; if(isTransfer) { shiftPosX = shiftPosX + transX * Time.deltaTime; shiftPosY = shiftPosY + transY * Time.deltaTime; worldPos.x += shiftPosX; worldPos.y += shiftPosY; worldPos.x += startPosX; worldPos.y += startPosY; trans.localScale *= transScale; } worldPos = CameraManager.Instance.PosFromMainCamToUICam(worldPos); worldPos.z = trans.position.z; } void ShowBoxPos() { if(curPos == worldPos) return; trans.position = worldPos; curPos = worldPos; } public void AttachObject(GameObject obj, Vector3 offset) { attachedObject = obj; attachedOffset = offset; InitHead(); } public void SetTransfer(bool isTrans) { isTransfer = isTrans; } public void setTransPos(float x,float y) { transX = x; transY = y; } public void setStartPos(float x,float y) { startPosX = x; startPosY = y; } public void setScale(float scale) { transScale = scale; } public void SetClipFar(float far) { ClipFar = far; } }
//继承UIHeadElement,使用PopEffect来设置动画状态,并控制每个Element的使用 using UnityEngine; public delegate void DestoryCB(PDamage pDamaga); public class UIDamagaElement :UIHeadElement { enum PopState { PopState_None, PopState_Ready, PopState_Loop, PopState_Finish, } DestoryCB destoryCB = null; PopState state = PopState.PopState_None; int curIndex= 0; float alpha = 0f; PDamage pDamaga; void Awake() { base.Awake(); } public void Init(PDamage damaga,DestoryCB destoryCB) { pDamaga = damaga; if(!this.gameObject.activeSelf) { this.gameObject.SetActive(true); } this.destoryCB = destoryCB; } void Update() { Tick(Time.deltaTime); } public void Tick(float time) { switch(state) { case PopState.PopState_None: curIndex = 0; state = PopState.PopState_Ready; this.SetTransfer(true); break; case PopState.PopState_Ready: { //初始化时间计数,设置好所有属性 pDamaga.effect.elementList[curIndex].Init(); if(!pDamaga.go.activeSelf) { pDamaga.go.SetActive(true); } //获取初始alpha值 alpha = pDamaga.effect.elementList[curIndex].startAlpha; //设置位移量和缩放比例 this.setStartPos(pDamaga.effect.elementList[curIndex].posx,pDamaga.effect.elementList[curIndex].posy); this.setTransPos(pDamaga.effect.elementList[curIndex].transferX,pDamaga.effect.elementList[curIndex].transferY); this.setScale(pDamaga.effect.elementList[curIndex].scale); //设置字当前的alpha pDamaga.lable.color = new Color(pDamaga.lable.color.r, pDamaga.lable.color.g, pDamaga.lable.color.b, alpha); state = PopState.PopState_Loop; break; } case PopState.PopState_Loop: { //记录重置的 pDamaga.effect.elementList[curIndex].Update(time); base.Tick(time); //设置字的alpha值 alpha += (pDamaga.effect.elementList[curIndex].alpha / (pDamaga.effect.elementList[curIndex].detailTime / time)); pDamaga.lable.color = new Color(pDamaga.lable.color.r, pDamaga.lable.color.g, pDamaga.lable.color.b, alpha); //判断是否结束 if (pDamaga.effect.elementList[curIndex].isEnd()) { curIndex ++; if(curIndex < pDamaga.effect.elementList.Count) state = PopState.PopState_Ready; //循环播放 else state = PopState.PopState_Finish; //停止播放 } break; } case PopState.PopState_Finish: { this.AttachObject(null, Vector3.zero); this.SetTransfer(false); Clear(); state = PopState.PopState_None; break; } default: break; } } void Clear() { if(destoryCB != null) { destoryCB(pDamaga); } curIndex= 0; alpha = 0f; } }
//提供两个相机之间 物体位置的转换,和距离的计算等 using UnityEngine; public class CameraManager : MonoBehaviour { private Camera MainCamera; private Camera UICamera; public static CameraManager Instance; void Awake () { MainCamera = GameObject.FindWithTag("MainCamera").GetComponent<Camera>(); UICamera = GameObject.FindWithTag("UICamera").GetComponent<Camera>(); Instance = this; } public Vector3 PosFromMainCamToUICam(Vector3 position) { if (MainCamera == null || UICamera == null) return Vector3.zero; Vector3 pos = MainCamera.WorldToViewportPoint(position); return UICamera.ViewportToWorldPoint(pos); } public float CalObjDepthFromCamera(float z) { if (MainCamera == null) return 0; return z - MainCamera.transform.position.z; } }
//伤害弹弹弹的管理器,负责初始化PDamage实例,缓存并使用PDamage //为某一个3D人物,添加伤害显示 using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class PopDamageManager : MonoBehaviour { public static PopDamageManager Instance; private Queue<PDamage> damagePool = new Queue<PDamage>(); private List<PDamage> activeDamage = new List<PDamage>(); public GameObject DamagePrefab; public Vector3 DefaultOffset = new Vector3(0, 0.6f, 0); void Awake () { Instance = this; } //初始化数量到对象池中,一般知道人物的多少,这些数量是可以预估出来的 public void Init(int damageCount) { if (DamagePrefab == null) return; for (int i = 0; i < damageCount; i++) { PDamage pDamage = new PDamage(); pDamage.go = Instantiate(DamagePrefab) as GameObject; pDamage.lable = pDamage.go.GetComponentInChildren<Text>(); pDamage.go.AddComponent<UIDamagaElement>(); damagePool.Enqueue(pDamage); pDamage.Clear(); } } //为go人物,添加一个伤害显示,指定好动画效果 public void AttachDamageToGameObject(GameObject go, PopEffect effect, string text) { if (damagePool.Count == 0) return; PDamage pDamage = damagePool.Dequeue(); pDamage.effect = effect; pDamage.lable.text = text; UIDamagaElement element = pDamage.go.GetComponent<UIDamagaElement>(); if (element != null) { element.AttachObject(go, DefaultOffset); element.Init(pDamage, RecoverDamage); } activeDamage.Add(pDamage); } //动画播放完成后就 隐藏回收到对象池 void RecoverDamage(PDamage node) { activeDamage.Remove(node); node.Clear(); damagePool.Enqueue(node); } public void Destory() { for (int i = 0; i < activeDamage.Count; i++) { damagePool.Enqueue(activeDamage[i]); } activeDamage.Clear(); PDamage node = null; while (damagePool.Count != 0 && (node = damagePool.Dequeue()) != null) { node.Destory(); } } }
三、测试
using UnityEngine; public class TestPop : MonoBehaviour { public GameObject Target; void Start() { PopDamageManager.Instance.Init(10); } private void Update() { if (Input.GetMouseButtonDown(0)) { int val = Random.Range(100, 1000); PopDamageManager.Instance.AttachDamageToGameObject(Target, new PopEffectHurt(), "-" + val); } if (Input.GetMouseButtonDown(1)) { PopDamageManager.Instance.Destory(); } } }
四、扩展
可以加上字体或则是字体颜色等,继承PopEffect创建更多的动画效果,达到暴击加血等等的不同效果。
通过BuildPipeLine的BuildPlayer方法,设置选项BuildOptions为BuildOptions.BuildAddttionalStreamedScene就可以将场景转化为一个可以动态加载的资源了。
string res = BuildPipeline.BuildPlayer( new string[] { "Assets/Scenes/scene_lobby.unity"},//场景资源的路径 destDirFileName, //存储为的目标文件路径 target, //平台 BuildTarget BuildOptions.BuildAdditionalStreamedScenes); //创建这个资源
通过上面创建出关卡场景的资源之后,就可以通过WWW.LoadFromCacheOrDownload(url,version)去加载指定的资源到内存中,就可以直接根据场景名字加载场景了
string sceneName = System.IO.Path.GetFileName(sceneFile); if (!Application.CanStreamedLevelBeLoaded(sceneName))//流模式的关卡是否已经加载 { yield return StartCoroutine(GameApp.ResMgr.LoadLevel(sceneFile));//没有加载的话,下载场景关卡文件 } ao = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(sceneName);
using System.Collections; using UnityEngine; public class PushTextShow : MonoBehaviour { public GameObject target; public float BigNum = 0.4f; //0 - 放大多少 public int BigFrame = 5; public float TanNum = 0.8f; //弹一弹的弧度 public int TanFrame = 5; //弹一弹的帧率 public float WaitTime = 1.5f;//展示的等待时间 public float XiaoShiFrame = 10;//消失的帧率 private void Awake() { if (target == null) target = this.gameObject; } void Start() { StartCoroutine(Ani()); } IEnumerator Ani() { //初始化 target.transform.localScale = Vector3.zero; float BigOff = (BigNum + 1) / BigFrame; //再放大 for (int i = 0; i < BigFrame; i++) { this.transform.localScale = new Vector3(target.transform.localScale.x + BigOff, target.transform.localScale.y + BigOff, target.transform.localScale.z); yield return new WaitForEndOfFrame(); } float TanOff = TanNum / TanFrame; //缩小放大弹一下 for (int i = 0; i < TanFrame; i++) { this.transform.localScale = new Vector3(target.transform.localScale.x - TanOff, target.transform.localScale.y - TanOff, target.transform.localScale.z); yield return new WaitForEndOfFrame(); } for (int i = 0; i < TanFrame; i++) { this.transform.localScale = new Vector3(target.transform.localScale.x + TanOff, target.transform.localScale.y + TanOff, target.transform.localScale.z); yield return new WaitForEndOfFrame(); } //先停留一下 yield return new WaitForSeconds(WaitTime); float XiaoShiOff = target.transform.localScale.x / XiaoShiFrame; //消失 for (int i = 0; i < XiaoShiFrame; i++) { this.transform.localScale = new Vector3(target.transform.localScale.x - XiaoShiOff, target.transform.localScale.y - XiaoShiOff, target.transform.localScale.z); yield return new WaitForEndOfFrame(); } //这里只是让效果重新播放 yield return new WaitForSeconds(0.2f); StartCoroutine(Ani()); } }
//根据视口坐标 更新UI的位置 private void UpdatePlayerUIPos() { Camera cc3d = Camera3d.GetComponent<Camera>(); for (int i = 0; i < GameLogic.GAME_PLAYER; i++) { if (i == 0) continue; //先是座椅位置在3D相机的视口,往上面走一下,椅子底面往上走到中间 Vector3 viewPoint = cc3d.WorldToViewportPoint(SeatCtr.Instance.char_roots[i].position + new Vector3(0,6f,0)); viewPoint = new Vector3(viewPoint.x, viewPoint.y,0); //3D视口转2D相机的世界坐标 o_player_nick[i].transform.parent.position = Camera.main.ViewportToWorldPoint(viewPoint); } }
无论是什么相机渲染到屏幕上都是限制在[0,1]之间的(视口坐标),通过这个作为中间体,两个相机可以通过这个将某一个相机渲染的东西的位置对应到另外一个相机渲染的位置上。视觉上就可以实现的就是昵称等UI跟着角色模型走。
bool isDrag = false; Vector3 originDraw; Vector3 upPos; private void OnMouseDown() { //记录原始位置 originDraw = Camera.main.ScreenToWorldPoint(Input.mousePosition); upPos = originDraw; } private void OnMouseDrag() { if (!isCanSelect) return; if (UIGame.isCantClickSelectCard) return; Vector3 nPos = Camera.main.ScreenToWorldPoint(Input.mousePosition); //设置牌的配置 transform.localPosition = new Vector3(transform.localPosition.x, transform.localPosition.y + (nPos.y - upPos.y) * 5,transform.localPosition.z); upPos = nPos; if ((nPos - originDraw).magnitude > 0.05f) { isDrag = true; } //判断y偏移 >= 0.5就起飞 有可能没打出去,回到原位 if (Mathf.Abs(nPos.y - originDraw.y) >= 0.3f) { transform.localPosition = mPos; UICardControl.CardData = this; CardCtl.target.SendMessage(MoveDownEvent); UICardControl.CardData = null; } } private void OnMouseUp() { if (isDrag) { transform.localPosition = mPos; isDrag = false; } }
mainCamera是一个UI相机来的,如果设置一个3D相机为主相机,无论怎么转换为世界坐标都是一定的值,所以用UI相机。记录滑动两次的偏移,使用你所需要的偏移对你的物体进行控制移动就好了。注意UI相机转换的滑动的世界坐标偏移,和实际你3D相机的世界坐标是有偏移的,所以我这里乘了5啊!角度问题?
很多看是滑动的开关,其实并没有滑动的,就是只有两个状态的直接改变,就好像是这样的
这里是开和关分别都是一张图片,开关的交互就是图片的切换,如果遇到这种情况,可以加一些缩放来增加切换的动感,按住扩大,释放缩小,这样就会有动感些了。
准备一张后面带有三个点的图片,表示正在什么种的意思的。做成UISprite,Type设置Filled,Dir为水平,如下
编写代码控制FillAmount变量,注意点的间隔,可以在可视化的时候测量出来或则做UI的时候固定好
void ShowHSZing(bool showed) { if (showed) { hsz_gameObject.SetActive(true); StartCoroutine(ShowHSZingAni()); } else { hsz_gameObject.SetActive(false); StopCoroutine(ShowHSZingAni()); } } IEnumerator ShowHSZingAni() { UISprite sprite = hsz_gameObject.transform.GetChild(0).GetComponent<UISprite>(); float from = 0.806f; float to = 0.938f; sprite.fillAmount = from; float speed = (to - from) / 3; int timer = 0; while (true) { sprite.fillAmount = sprite.fillAmount + speed; timer++; yield return new WaitForSeconds(0.2f); if (timer % 3 == 0) { sprite.fillAmount = from; } } }
结果:
打开Crazybump后,打开贴图(就是平常的那张模型贴图),然后调节参数如下(正常情况下,我试了很多次试出来的):
Intensity是强度的意思,负数就是凹下去的,正数的就是凸起来的
保留Medium Detail细节,其他都不需要的,不然会有偏移等,看具体情况
好了,导出为图片再导入到Unity中,更改Shader使用法线贴图,附上这个法线贴图,这样立体感就出来了。
原来的
添加法线后(强度系数为0.6)
思路是这样子的,先是保存原来物体的信息(目标位置)(包括位置、旋转、 还有父物体等信息),然后就可以修改物体的信息,使它可以展示给相机正面去看到了。怎么让相机正面看到这个物体呢?我想到最好的一个办法就是设置它为相机下的子物体,然后就可以利用参考性的本地坐标来去实现了,pos位置就是参考相机的位置,这样的就很好知道数值了,设置好位置后,注意角度,展示的那个面,这里是-z轴的面,LookAt面向相机就行了,设置本地坐标全为0后就是相机的旋转了,再根据x,z的偏移计算出y轴的视觉偏差,纠正回来就ok了。展示完后,回到原来位置就好了。
IEnumerator OutCardAni(UICardControl control,Vector3 pos) { if (control.allCards.Count < 1) yield break; Transform lastCard = control.allCards[control.allCards.Count - 1].transform; Vector3 oPos = lastCard.position; Vector3 oRot = lastCard.localEulerAngles; //展示给相机 Transform oParent = lastCard.parent; lastCard.parent = Camera3d; lastCard.localPosition = pos; //lastCard.LookAt(-Camera3d.transform.position); //旋转一下偏移,3D会有视觉偏差 lastCard.localEulerAngles = new Vector3(0, Mathf.Atan2(pos.x, pos.z) * Mathf.PI * 12, 0); //Tan的角度 //lastCard.localScale = new Vector3(0.8f,0.8f, 0.8f); yield return new WaitForSeconds(0.5f); if (lastCard == null) yield break; lastCard.parent = oParent; lastCard.localScale = Vector3.one; //yield break; int frame = 5; Vector3 offP = (lastCard.position - oPos) / frame; Vector3 offR = (lastCard.localEulerAngles - oRot) / frame; for (int i = 0; i < frame; i++) { if (lastCard == null) yield break; lastCard.position = lastCard.position - offP; lastCard.localEulerAngles = lastCard.localEulerAngles - offR; yield return new WaitForEndOfFrame(); } //音效 PlayGameSound(SoundType.OUTCARD); //箭头指向他 if (lastCard != null) { SetDiscardArrow(true, lastCard.transform); } //yield return new WaitForEndOfFrame(); }
UI的组成元素主要有两个,一个外框,一个圆,创建一个父物体,用来接受点击和拖拽事件,设置框和圆为父物体的子物体。父物体的碰撞体可以做大一点,点击到区域内的,调整父物体的位置到点击的区域,圆的相对坐标设为0。平常不用虚拟摇杆的时候可以设置框和圆的透明度为0.5那样,点击和出发拖拽再tween到1。拖拽OnDraw(Vector2 v)-NGUI,IDragHandler.OnDrag(PointerEventData d)触发计算设置圆的位置,注意限制在框的半径中,可以用Distance判断。
计算摇杆的方向,然后映射到主角的移动方向上
//计算摇杆控制的方向 Vector3 direction = point.position - transform.position; direction = new Vector3(direction.x, 0f, direction.y); direction.Normalize(); //映射到目标模型上,调整目标方向 target.LookAt(target.position + direction); //计算相机的旋转偏差 Quaternion rot = Quaternion.Euler(0, camera.transform.eulerAngles.y, 0); target.LookAt(rot * target.forward);
在3D的世界中,观察世界的相机不一定就是正方向的去观察世界的,也许是45的,所以当将摇杆方向映射到主角时,也需要考虑相机的角度。
比如说:摇杆方向(圆心在正上),映射方向为(0,0,1),主角向前走,相机无旋转在主角z轴后面对着它,刚好圆心向上,主角在相机的视野中也是向上(前)。当相机y轴以45°观察时,就没有对到主角的z轴,主角还是向前走,但是在视野中确不是向上了(不和摇杆同一方向),而是往左上走了,所以需要将方向同相机一样旋转45度纠正回来,达到视野的移动方向和摇杆的一致。