lab6

来源:互联网 发布:芸豆会计软件免费版 编辑:程序博客网 时间:2024/05/21 12:43

简陋射鸡游戏

  本来老师的意思应该是做一个保龄球类的游戏(扔石头砸一窝鸡),然而由于我渣渣的阅读理解,将错就错做成了射击游戏。
这里写图片描述
  这个界面真是蠢蠢的,而且还是俯视图。做成俯视图主要是我发现想打中不容易,所以调整了摄像机。

目录

  • 简陋射鸡游戏
    • 目录
      • 游戏内容
      • 结构图
      • 框架脚本
      • 预制件脚本
      • 错误总结
      • 展望

1.游戏内容

  一群鸡在地图上乱跑,玩家拿石头砸死它们。鸡的行动方向是随机的,所以要趁鸡决定转向前预判砸死。游戏没有失分机制,但是会记录使用的石头数量,另外打中鸡的头有更高的分数。

2.结构图

这里写图片描述
  这是基本的框架,采用了MVC架构。
  实际的脚本如下:
  这里写图片描述
  这里看到多出来了3个脚本,其实是用来挂载在预制件上的。由于我的代码似乎比较丑陋的因素,unity在完成上述的结构之后已经偶尔会崩溃掉,为了不增加复杂性就没有考虑制作动作工厂(嫌烦……)。

3.框架脚本

  首先,制作了鸡工厂。
  

using System.Collections.Generic;using UnityEngine;using Com.Mygame;namespace Com.Mygame{    public class ChickenFactory : System.Object    {        private static ChickenFactory _instance;        private static List<GameObject> chickenList;        public GameObject chickenTemplate;        public static ChickenFactory getInstance()        {            if (_instance == null)            {                _instance = new ChickenFactory();                chickenList = new List<GameObject>();            }            return _instance;        }        public int getChicken()        {            for (int i = 0; i < chickenList.Count; i++)            {                if (!chickenList[i].activeInHierarchy) return i;            }            chickenList.Add(GameObject.Instantiate(chickenTemplate) as GameObject);            return chickenList.Count - 1;        }        public GameObject getChickenObject(int id)        {            if (id > -1 && id < chickenList.Count) return chickenList[id];            return null;        }        public void Free(int id)        {            if (id > -1&&id < chickenList.Count)            {                Debug.Log("now free it!");                chickenList[id].GetComponent<Rigidbody>().velocity = Vector3.zero;                chickenList[id].SetActive(false);                ChickenMove cm = (ChickenMove)chickenList[id].GetComponent("ChickenMove");                cm.setDead("none");   // 因为是回收利用,要把死因重置            }        }    }}public class ChickenFactoryBC : MonoBehaviour{    public GameObject chicken;    private void Awake()    {        chicken = Resources.Load("chicken") as GameObject;        // Debug.Log("prefab chicken loaded");        ChickenFactory.getInstance().chickenTemplate = chicken;    }}

  鸡工厂负责产生和回收鸡,这里注意到鸡工厂用的基类是System.Object。这是因为Monobehavior不支持用new的方法实现单例类。因此额外用另一个类型为Monobehavior的ChickenFactoryBC来做加载预制件的内容。
  完成了鸡工厂后是场景控制器,这里写在了BaseCode中。
  

using UnityEngine;using Com.Mygame;namespace Com.Mygame{    public interface IUserInterface    {        void startChicken();        void HasShot();        int getShot();    }    public interface IQueryStatus    {        bool isCounting();        bool isShooting();        int getRound();        int getPoint();        int getEmitTime();    }    public interface IJugdeEvent    {        void nextRound();        void setPoint(int point);    }    public class SceneController : System.Object, IQueryStatus, IUserInterface, IJugdeEvent    {        private static SceneController _instance;        private BaseCode _baseCode;        private GameModel _gameModel;        private Judge _judge;        private int _round;        private int _point;        private int _shot; // 记录用了几个球        public static SceneController getInstance()        {            if (_instance == null)            {                _instance = new SceneController();            }            return _instance;        }        public int getShot() { return _shot; }        public void HasShot() { _shot++; }        public void startChicken() {            Debug.Log("prepare to release chicken!");            _gameModel.prepareToChicken();        }        public void setGameModel(GameModel obj) { _gameModel = obj; }        internal GameModel getGameModel() { return _gameModel; }        public void setJudge(Judge obj) { _judge = obj; }        internal Judge getJudge() { return _judge; }        public void setBaseCode(BaseCode obj) { _baseCode = obj; }        internal BaseCode getBaseCode() { return _baseCode; }        public bool isCounting() { return _gameModel.isCounting(); }        public bool isShooting() { return _gameModel.isShooting(); }        public int getRound() { return _round; }        public int getPoint() { return _point; }        public int getEmitTime() { return (int)_gameModel.timeToChicken + 1; }        // 得分接口          public void setPoint(int point) { _point = point; }        public void nextRound() {            _point = 0;            _baseCode.LoadRoundData(++_round);        }    }    public class BaseCode : MonoBehaviour    {        private Vector3 _center;        private float _radius; // 和ChickenMove中不一样,是用来判断生成范围的        private float speed;        void Awake()        {            SceneController.getInstance().setBaseCode(this);            _center = new Vector3(0, 0, 8);            _radius = 15f;            speed = 0.2f;            // Debug.Log("what happen?");        }        public void LoadRoundData(int round)        {            speed *= round;            SceneController.getInstance().getGameModel().setting(speed, round, _center, _radius);        }    }}

  场景控制器中实现了各种查询函数,可以查询到是否可以开始射击、是否正在倒数等信息。在BaseCode的LoadRoundData中,关卡的信息没有直接写死,理论上可以一直打到第N关。
  接下来是用户接口,它使用场景控制器提供的信息来控制用户交互。因为只有一个子弹,因此子弹落地之前都不能再次射击(避免玩家搞火力覆盖),这里用子弹的位置信息来加以判断。每次子弹被回收后位置会重置为世界坐标原点。
  

using UnityEngine;using Com.Mygame;using UnityEngine.UI;public class UserInterface : MonoBehaviour{    public Text mainText; // 当前回合数    public Text scoreText; // 得分数    private int round;    public GameObject stone;    public float forcePower;    private IUserInterface userInt;    private IQueryStatus queryInt;    // Use this for initialization    void Start()    {        mainText = transform.Find("Canvas/MainText").GetComponent<Text>();        scoreText = transform.Find("Canvas/ScoreText").GetComponent<Text>();        stone = Instantiate(Resources.Load<GameObject>("stone")) as GameObject;        stone.SetActive(false);        userInt = SceneController.getInstance() as IUserInterface;        queryInt = SceneController.getInstance() as IQueryStatus;        forcePower = 10f;    }    // Update is called once per frame    void Update()    {        if (queryInt.isCounting())        {            mainText.text = (queryInt.getEmitTime()).ToString();        }        else        {            if (Input.GetKeyDown("space")) { userInt.startChicken(); } // 这个函数启动鸡的生成            if (queryInt.isShooting()) { mainText.text = ""; }        }        if (queryInt.isShooting() && Input.GetMouseButtonDown(0) && stone.transform.position == Vector3.zero)        {            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);            stone.GetComponent<Rigidbody>().velocity = Vector3.zero;            stone.GetComponent<Transform>().position = transform.position;            stone.SetActive(true);            stone.GetComponent<Rigidbody>().AddForce(ray.direction * forcePower, ForceMode.Impulse);            userInt.HasShot();        }        if (!queryInt.isCounting())        {            mainText.text = "Round: " + queryInt.getRound().ToString();            mainText.color = Color.red;            scoreText.text = "Score: " + queryInt.getPoint().ToString() + "\n" + "Stone used: "                + userInt.getShot().ToString();            scoreText.color = Color.green;        }        if (round != queryInt.getRound())        {            round = queryInt.getRound();            mainText.text = "Round " + round.ToString() + " !";        }    }}

  unity3d 5.0的版本必须用画布来使用text,所以可以看到我把位置写成了”Canvas/text”。
  然后是游戏模型和规则类。

using System.Collections.Generic;using UnityEngine;using Com.Mygame;public class GameModel : MonoBehaviour {    public float countDown = 3f;    public float timeToChicken;    private bool counting;    private bool shooting;    public bool isCounting() { return counting; }    public bool isShooting() { return shooting; }    private List<GameObject> chicken = new List<GameObject>();    private List<int> chickenIds = new List<int>();    private float speed;            // 鸡的移动速度    public int chickenNum;         // 要生成的鸡数目    private bool NewChicEnable;     // 是否有新的鸡生成    private Vector3 c = Vector3.zero;    private float r = 10f;    private SceneController scene;    void Awake()    {        countDown = 3f;        scene = SceneController.getInstance();        scene.setGameModel(this);        Instantiate(Resources.Load("Plane"), new Vector3(0, 0, 9), Quaternion.identity);    }    public void stoneisflying()    {        shooting = false;    }    public void stoneendflying()    {        shooting = true;    }    public void setting(float _sp, int _ro, Vector3 center, float radius)    {        speed = _sp;        chickenNum = _ro;        c = center;        r = radius;    }    public void prepareToChicken()    {        if (!counting && !shooting)        {            timeToChicken = countDown;            NewChicEnable = true;        }    }    void GenChicken()    {        for (int i = 0; i < chickenNum; ++i)        {            chickenIds.Add(ChickenFactory.getInstance().getChicken());            chicken.Add(ChickenFactory.getInstance().getChickenObject(chickenIds[i]));            chicken[i].SetActive(true);            chicken[i].transform.position = validRandom();            ChickenMove cm = (ChickenMove)chicken[i].GetComponent("ChickenMove");            cm.reinitialize();            cm.speed *= speed;        }    }    public Vector3 validRandom()    {        Vector3 a = new Vector3(0, 0, 0);        int distance = (int)(c.x - r);        bool valid = false;        while (!valid)        {            int x = Random.Range(-distance, distance);            int z = Random.Range(-distance, distance);            a.x += x; a.z += z;            if ((a - c).magnitude <= r) valid = true;        }        return a;    }    void FreeChicken(int i)    {        ChickenFactory.getInstance().Free(i);        chicken.RemoveAt(i);        chickenIds.RemoveAt(i);    }    void FixedUpdate()    {        if (timeToChicken > 0)        {            counting = true;            timeToChicken -= Time.deltaTime;        }        else        {            counting = false;            if (NewChicEnable)            {                GenChicken();                NewChicEnable = false;                shooting = true;                Debug.Log("shoot available now");            }        }    }    // Update is called once per frame    void Update () {        for (int i = 0; i < chicken.Count; ++i)        {            ChickenMove cm = (ChickenMove)chicken[i].GetComponent("ChickenMove");            if (cm.WhyDead() != "none")            {                // Debug.Log("get one judged for " + cm.WhyDead());                scene.getJudge().ShotAChicken(cm.WhyDead());                FreeChicken(i);            }            // 没有失分规则            if (chicken.Count == 0) {                // Debug.Log("now shot disallowed");                shooting = false;            }        }    }}
using UnityEngine;using Com.Mygame;public class Judge : MonoBehaviour {    public int shotHeadScore = 50;    public int shotOtherScore = 10;    public int ScoreToWin = 20;    private SceneController scene;    void Awake()    {        scene = SceneController.getInstance();        scene.setJudge(this);    }    // Use this for initialization    void Start () {        Debug.Log("round one default");        scene.nextRound();    }    public void ShotAChicken(string tag)    {        if (tag == "chicken")        {            scene.setPoint(scene.getPoint() + shotOtherScore);        }        else if (tag == "head")        {            scene.setPoint(scene.getPoint() + shotHeadScore);        }        if (scene.getPoint() >= ScoreToWin)        {            ScoreToWin = (scene.getRound()+1) * 10 + 20;            scene.nextRound();        }    }}

  因为没有动作工厂,所以好几个地方都直接通过鸡身上的脚本来获取信息,比如死因。

4.预制件脚本

  下面是挂载在鸡身上的脚本,一个控制行动的ChickenMove和一个检测碰撞的GetHit,其实这两个可以写成一个,但为了方便给鸡的头部也添加碰撞检测,所以分离了。其实这里应该可以通过stone的碰撞检测间接地获知鸡死因等信息的。鸡的死因最好是不要记录在鸡身上的脚本里,否则回收利用时一定要重新初始化数据。这里是个反面教材。
  

using UnityEngine;public class ChickenMove : MonoBehaviour {    private Vector3 _center;    private int _radius;    private float time_wait;    private float current_time;    private Vector3 direction;    private Vector3 last_direction;    public float speed;    private string deadCause;    private Animation ani;    private bool canTurn;    public string WhyDead() { return deadCause; }    // Use this for initialization    private void Awake()    {        deadCause = "none";        speed = 40f;        ani = GetComponent<Animation>();        ani.wrapMode = WrapMode.Loop;    }    void Start () {        _center = transform.position;        _radius = 5;        time_wait = 3f;        current_time = time_wait;        direction = new Vector3(0, 0, -1);        ani.Play("run");        canTurn = true;    }    public Vector3 ran_pos()    {        Vector3 a = new Vector3(0, 0, 0);        int x = Random.Range(-10, 10);        int z = Random.Range(-10, 10);        a.x += x; a.z += z;        a = a.normalized;        return a;    }    void ChangeDirection()    {        last_direction = direction;        direction = ran_pos();        float yt = AngleCal(direction);        transform.Rotate(new Vector3(0, yt, 0));    }    public void TurnAround()    {        last_direction = direction;        Vector3 a = Vector3.zero;        a.x -= direction.x;        a.z -= direction.z;        a = a.normalized;        direction = a;        float yt = AngleCal(direction);        transform.Rotate(new Vector3(0, yt, 0));    }    public float AngleCal(Vector3 target)    {        Vector3 toTurn = Vector3.Cross(last_direction, target);        if (toTurn.y > 0) return Vector3.Angle(last_direction, target);        else return 360- Vector3.Angle(last_direction, target);    }    public void reinitialize()    {        Start();    }    public void setDead(string reason) { deadCause = reason; }    void Update()    {        current_time -= Time.deltaTime;        if (((transform.position + GetComponent<Rigidbody>().velocity - _center).magnitude > _radius) && canTurn)        {            TurnAround();            canTurn = false; // 进行一次触边转头之后接下来暂停检测触边            current_time = time_wait; // 重置转向时间        }        else if (current_time <= 0)        {            current_time = time_wait;            ChangeDirection();            // Debug.Log("turn");            // GetComponent<Rigidbody>().velocity = Vector3.zero;        }        else        {            GetComponent<Rigidbody>().MovePosition(transform.position + direction * speed * Time.deltaTime);            // 勾选了isKinematic,不能使用velocity        }    }}

  这里的重点在于计算鸡的旋转角度。我用了一个函数来算出y轴旋转度。这个计算方法需要一点理解。
  另外,这是鸡的预制件:
  这里写图片描述
  首先,为了能够检测碰撞,要给鸡加上碰撞器,这里用了最简单的盒型碰撞器。
  另外,碰撞有一个副作用,那就是会施加物理的力,如果你不想在工厂里写代码修正鸡的初始旋转角,就勾选isKinematic选项,让鸡不受外力影响。勾选这个选项后,也不能使用外力来移动力,所以我采用了rigidbody.MovePosition的方法。
  这里还可以看到Interpolate被选中了,这是内插值,其功能是平滑刚体的移动。这个功能非常有效,stone也要同样处理。
  

using UnityEngine;public class GetHit : MonoBehaviour {    private Animation ani;    private GameObject father;    private ChickenMove cm;    private void Awake()    {        father = GetFather(this.gameObject).gameObject;        cm = (ChickenMove)father.GetComponent("ChickenMove");        ani = father.GetComponent<Animation>();    }    private void OnCollisionEnter(Collision other)    {        Debug.Log("who hit me? " + other.transform.tag);        if (other.transform.tag == "stone")        {            cm.setDead(transform.tag);            Debug.Log("die with " + transform.tag + " being hit");            ani.Play("death", PlayMode.StopAll);        }        else if (other.transform.tag == "chicken") cm.TurnAround();    }    public GameObject GetFather(GameObject son)    {        while (son.transform.parent != null)        {            son = son.transform.parent.gameObject;        }        return son;    }}

  碰撞检测脚本。里面写了一个获取最高父级的函数,这是预备给鸡头用的,也可以给其他部件用。方便被击中时通知鸡死亡。
  下面这个是stone上挂的碰撞检测脚本,写了两个碰撞脚本很多余。但我没时间改了,实在是最开始的时候碰撞的检测条件不清楚,花了很多时间测试。石头由于没有被工厂管理,所以碰撞脚本里已经写好了对自己的回收。石头的使用和回收就是依靠这个脚本和用户接口类完成的。

using UnityEngine;public class StoneHit : MonoBehaviour{    private void OnCollisionEnter(Collision other)    {        if (other != null)        {            // Debug.Log("hit something: " + other.transform.tag);            transform.gameObject.SetActive(false);            transform.position = Vector3.zero;        }    }    private void Update()    {        if (transform.position.y < 0)        {            transform.gameObject.SetActive(false);            transform.position = Vector3.zero;        }    }}

  剩余的两个预制件如下:
 这里写图片描述
  这是地板,没有勾选碰撞器是为了避免鸡和它发生碰撞。根据网上的说法,也可以设置layer的碰撞关系来达到这一目的,这里就简单处理了。因为鸡没有勾选重力,地板其实只是用来看的,不参与其他事件。
  这里写图片描述
  这就是这个游戏的子弹了,同样选取了内插值。

5.错误总结

  什么都别说了,高手和低手的差别真大。我不熟悉unity,写这个简陋的游戏已经欲仙欲死,花了很多时间,走了不少冤枉路。虽然这样,但是也算是有点收获,起码下次不会这么残。把这次遇到的各种问题记录下来,以资后用(也有一些还没完全清楚的,可以继续研究)。
  1.内插值可以实现平滑移动。
  2.数据的初始化最好全部写到Start()Awake()中,直接在声明时写是没用的(但也不会提示你错误!)。另外Awake()的执行在对象实例化时就完成了,其他脚本中实例化对象后马上就要使用的数据,例如单例的_instance,必须在Awake里才有效,否则会NullReferenceException。
  3.高速物体用离散检测碰撞是检测不到的。
  4.游戏模型中不涉及碰撞的对象千万不要加碰撞体,例如地板,否则会与其他对象产生不可预知的后果。
  5.要让animation播放多个动画,必须首先在Animations中把动画加入。注意别加错了同名的其他模型的动画,不会报错但是没有效果。bird包里就有个rooster的run动画……

6.展望

  虽然写得逊毙了还各种崩溃,但我总算是这几个星期里第一次这么认真搞unity。都是这个博客逼的……unity的asset shop貌似不能直接连上还得挂vpn,所以我拿之前搬的行星贴图直接用了,地板用的是月球,石头用的是太阳(食我太阳拳啦)。写完之后虽然简陋爆了,代码也很样衰,还是有成就感。
  稍微会感到一点趣味了……但是,昨天写得太痴迷了忘记上公选了……
  这里厚脸皮说一下,虽然应该不会有人参考我的代码,但一定要小心,我这里最有用的就是那些错误心得和算旋转角的函数,其他都很危险。因为我的unity崩溃了很多次!

0 0
原创粉丝点击