在下Simba,有何贵干!新一期的游戏又出炉了(明明就是个Demo啊喂),这次的游戏比以往都简单(你别骗我),但是比以往的都好看(Excuse me?),没错,那就是动画!这一期的游戏使用了以往都没有使用过的动画系统,而且用了别人模型(不要脸)。
Garen模型:http://pan.baidu.com/s/1i5exKNV
使用前请将所有模型和动作设置成Legacy动画, inspector -> Rig -> Animation type -> Legacy -> Apply button。
先来看看这酷炫的效果吧:(这...这莫非是...盖伦?)
游戏的规则很简单,玩家控制盖伦击打完场上的7个球游戏结束,并给出最终用时。
游戏的类图:
(说好的很简单呢?我怎么看不懂)不急不急,看上去很复杂,其实只有3个脚本要写,话说这图可能还有错,刚学的UML作图。
步骤说明:
一、场景布置
场景布置总是游戏开始制作时必须考虑和完善的事情,在打代码前得脑补出游戏正常运行时的情景。我设计的游戏摄像机是不动的,玩家可以在固定区域内通过WASD移动,点击鼠标左键进行攻击。当区域内所有小球都被击打消失后,游戏结束,显示玩家用时。
首先把玩家Garen拖入场景中,reset位置。新建4个正方体和1个平面,通过拉伸和位移组成一个方块区域:
给玩家添加刚体组件和碰撞盒,碰撞盒采用胶囊体:
新建1个UI Text对象,并将其定位在屏幕中央,用于显示玩家的最终用时:
场景中的所有对象:
二、玩家动作
OK,可以开始写代码了。想从最直观的写起,那么就先写玩家的控制吧。玩家有3个动作,分别是Idle、Run、Attack。Idle在Start时就播放,Run需要在Update中检测键盘输入,可以使用GetAxisRaw获得WASD的方向。Attack是比较复杂的动作,通过在动作过程中注册回调函数,可以使得在攻击动画播放过程中触发这些函数,比如AttackHit函数,以及StopAttack函数。前者判断有没有击中物体,并广播击中消息。后者使玩家播放站立动画(攻击动画结束后站立)。
using UnityEngine; using System.Collections; public class GarenMovement : MonoBehaviour { Animation ani; AnimationState idle; AnimationState run; AnimationState attack; public float speed = 5f; Vector3 movement; Rigidbody playerRigidbody; bool isAttacking = false; float rayLength = 1.8f; public delegate void AttackHitHandler(GameObject obj); public static event AttackHitHandler OnAttackHit; void Start() { playerRigidbody = this.GetComponent<Rigidbody>(); ani = this.GetComponent<Animation>(); idle = ani["Idle"]; run = ani["Run"]; attack = ani["Attack1"]; // 默认播放站立动画 idle.wrapMode = WrapMode.Loop; ani.Play(idle.clip.name); } void FixedUpdate () { if (!isAttacking) { float h = Input.GetAxisRaw("Horizontal"); float v = Input.GetAxisRaw("Vertical"); Move(h, v); } if (Input.GetMouseButtonDown(0)) { Attack(); } } void Move(float h, float v) { if (h != 0 || v != 0) { movement.Set(h, 0f, v); // 移动玩家位置 movement = movement.normalized * speed * Time.deltaTime; playerRigidbody.MovePosition(transform.position + movement); // 旋转玩家角度 Quaternion newRotation = Quaternion.LookRotation(movement); playerRigidbody.MoveRotation(newRotation); // 播放跑步动画 run.wrapMode = WrapMode.Loop; ani.CrossFade(run.clip.name, 0.1f); } else { ani.CrossFade(idle.clip.name, 0.1f); } } void Attack() { isAttacking = true; if (attack.clip.events.Length == 0) { // 添加攻击动画结束后的回调函数 AnimationEvent endEvent = new AnimationEvent(); endEvent.functionName = "StopAttack"; endEvent.time = attack.clip.length - 0.2f; attack.clip.AddEvent(endEvent); // 添加攻击动画中的回调函数 AnimationEvent hitEvent = new AnimationEvent(); hitEvent.functionName = "AttackHit"; hitEvent.time = 0.5f; attack.clip.AddEvent(hitEvent); } ani.Play(attack.clip.name); } void StopAttack() { isAttacking = false; } void AttackHit() { // 射线判断打击物 GameObject obj = GameObject.Find("C_BUFFBONE_GLB_CENTER_LOC"); Ray ray = new Ray(obj.transform.position, movement); RaycastHit hit; if (Physics.Raycast(ray, out hit, rayLength)) { Debug.DrawLine(ray.origin, hit.point); OnAttackHit(hit.collider.gameObject); } } }
射线碰撞检测非常有意思,如果单纯的靠网格碰撞器来检测击打物的话,至少我是没想到什么好方法,貌似网格碰撞检测都是被动的,主动的只有射线检测。这里的Trick是把射线的长度设置为刀的长度,然后发出点设为Garen的腰部,这就可以很好地检测Garen的刀是否“砍”中了物体。
另外,Delegate对象完成了UML图中的AttackHitHandler和HitEvent类的工作,因此实际上代码并没有那么复杂。什么是Delegate呢?这好比是一个公众号,任何人都可以关注它。突然某一天,公众号宣布科比退役了!如果是关注了这个公众号的人就可以立马知道这个新闻,但是每个人都可以做出不同的反应,或伤心或开心,因人而因,和公众号就没有关系了。Delegate只负责广播新闻,却不去追究新闻发布后的结果。
三、裁判类
现在玩家可以执行各种动作了,可是我还不知道我的AttackHitHandler是否能正常工作,于是赶忙写了裁判Judge类来验证一下:
using UnityEngine; using System.Collections; using Com.mygame; public class Judge : MonoBehaviour { public int count = 7; void Start () { GarenMovement.OnAttackHit += HitEvent; } void HitEvent(GameObject obj) { if(obj.tag.Contains("Ball")) { obj.SetActive(false); if (--count == 0) { MyUI.GetInstance().Display(Time.time.ToString()); } } } }
其中,UI是后来改的,没写时可以用print或Debug来测试。记得添加Tag。
四、工厂类
现在得考虑球体了,创建一个工厂来管理这些球体是个不错的方法,新建BaseCode脚本用来写单例类吧,顺便定义一个命名空间:
using UnityEngine; using UnityEngine.UI; using System.Collections; using System.Collections.Generic; using Com.mygame; namespace Com.mygame { public class BallFactory : System.Object { static BallFactory instance; static List<GameObject> ballList; public static BallFactory GetInstance() { if (instance == null) { instance = new BallFactory(); ballList = new List<GameObject>(); } return instance; } public GameObject GetBall() { for (int i = 0; i < ballList.Count; ++i) { if (!ballList[i].activeInHierarchy) { return ballList[i]; } } GameObject newObj = GameObject.CreatePrimitive(PrimitiveType.Sphere); newObj.GetComponent<Renderer>().material.color = Color.green; newObj.tag = "Ball"; ballList.Add(newObj); return newObj; } } }考虑到回收完全可以通过ball.SetActive(false)完成,就让Judge来完成了。
五、UI
UI这么重要的东西这么可以忘掉,在命名空间内加上:
public class MyUI : System.Object { static MyUI instance; public Text mainText; public static MyUI GetInstance() { if (instance == null) { instance = new MyUI(); } return instance; } public void Display(string info) { mainText.text = info; } }
六、场景初始化
万事俱备,只欠初始了。BaseCode刚好可以用来初始化场景。Start在区域内随机位置生成7个球,并把MyUI的mainText对象赋值:
public class BaseCode : MonoBehaviour { public int balls = 7; public Text text; void Start() { MyUI.GetInstance().mainText = text; for (int i = 0; i < balls; ++i) { GameObject ball = BallFactory.GetInstance().GetBall(); ball.transform.position = new Vector3(Random.Range(-10f, 10f), 1f, Random.Range(-10f, 10f)); } } }
全部代码
GarenMovement.cs
using UnityEngine; using System.Collections; public class GarenMovement : MonoBehaviour { Animation ani; AnimationState idle; AnimationState run; AnimationState attack; public float speed = 5f; Vector3 movement; Rigidbody playerRigidbody; bool isAttacking = false; float rayLength = 1.8f; public delegate void AttackHitHandler(GameObject obj); public static event AttackHitHandler OnAttackHit; void Start() { playerRigidbody = this.GetComponent<Rigidbody>(); ani = this.GetComponent<Animation>(); idle = ani["Idle"]; run = ani["Run"]; attack = ani["Attack1"]; // 默认播放站立动画 idle.wrapMode = WrapMode.Loop; ani.Play(idle.clip.name); } void FixedUpdate () { if (!isAttacking) { float h = Input.GetAxisRaw("Horizontal"); float v = Input.GetAxisRaw("Vertical"); Move(h, v); } if (Input.GetMouseButtonDown(0)) { Attack(); } } void Move(float h, float v) { if (h != 0 || v != 0) { movement.Set(h, 0f, v); // 移动玩家位置 movement = movement.normalized * speed * Time.deltaTime; playerRigidbody.MovePosition(transform.position + movement); // 旋转玩家角度 Quaternion newRotation = Quaternion.LookRotation(movement); playerRigidbody.MoveRotation(newRotation); // 播放跑步动画 run.wrapMode = WrapMode.Loop; ani.CrossFade(run.clip.name, 0.1f); } else { ani.CrossFade(idle.clip.name, 0.1f); } } void Attack() { isAttacking = true; if (attack.clip.events.Length == 0) { // 添加攻击动画结束后的回调函数 AnimationEvent endEvent = new AnimationEvent(); endEvent.functionName = "StopAttack"; endEvent.time = attack.clip.length - 0.2f; attack.clip.AddEvent(endEvent); // 添加攻击动画中的回调函数 AnimationEvent hitEvent = new AnimationEvent(); hitEvent.functionName = "AttackHit"; hitEvent.time = 0.5f; attack.clip.AddEvent(hitEvent); } ani.Play(attack.clip.name); } void StopAttack() { isAttacking = false; } void AttackHit() { // 射线判断打击物 GameObject obj = GameObject.Find("C_BUFFBONE_GLB_CENTER_LOC"); Ray ray = new Ray(obj.transform.position, movement); RaycastHit hit; if (Physics.Raycast(ray, out hit, rayLength)) { Debug.DrawLine(ray.origin, hit.point); OnAttackHit(hit.collider.gameObject); } } }
Judge.cs
using UnityEngine; using System.Collections; using Com.mygame; public class Judge : MonoBehaviour { public int count = 7; void Start () { GarenMovement.OnAttackHit += HitEvent; } void HitEvent(GameObject obj) { if(obj.tag.Contains("Ball")) { obj.SetActive(false); if (--count == 0) { MyUI.GetInstance().Display(Time.time.ToString()); } } } }
BaseCode.cs
using UnityEngine; using UnityEngine.UI; using System.Collections; using System.Collections.Generic; using Com.mygame; namespace Com.mygame { public class MyUI : System.Object { static MyUI instance; public Text mainText; public static MyUI GetInstance() { if (instance == null) { instance = new MyUI(); } return instance; } public void Display(string info) { mainText.text = info; } } public class BallFactory : System.Object { static BallFactory instance; static List<GameObject> ballList; public static BallFactory GetInstance() { if (instance == null) { instance = new BallFactory(); ballList = new List<GameObject>(); } return instance; } public GameObject GetBall() { for (int i = 0; i < ballList.Count; ++i) { if (!ballList[i].activeInHierarchy) { return ballList[i]; } } GameObject newObj = GameObject.CreatePrimitive(PrimitiveType.Sphere); newObj.GetComponent<Renderer>().material.color = Color.green; newObj.tag = "Ball"; ballList.Add(newObj); return newObj; } } } public class BaseCode : MonoBehaviour { public int balls = 7; public Text text; void Start() { MyUI.GetInstance().mainText = text; for (int i = 0; i < balls; ++i) { GameObject ball = BallFactory.GetInstance().GetBall(); ball.transform.position = new Vector3(Random.Range(-10f, 10f), 1f, Random.Range(-10f, 10f)); } } }