Unity3D学习笔记(7)—— 击球游戏

        在下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个平面,通过拉伸和位移组成一个方块区域:

Unity3D学习笔记(7)—— 击球游戏_第1张图片

        给玩家添加刚体组件和碰撞盒,碰撞盒采用胶囊体:



Unity3D学习笔记(7)—— 击球游戏_第2张图片

        新建1个UI Text对象,并将其定位在屏幕中央,用于显示玩家的最终用时:

Unity3D学习笔记(7)—— 击球游戏_第3张图片

        场景中的所有对象:

Unity3D学习笔记(7)—— 击球游戏_第4张图片

二、玩家动作

        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));
        }
    }
}

        代码写完了(简单吧,才200行),得把它们挂载在游戏物体上,我把GarenMovement脚本挂载在了玩家Garen上,把BallFactory和BaseCode挂载在了MainCamera上,运行一下,OK!


全部代码

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));
        }
    }
}

你可能感兴趣的:(C#,unity3d)