lab6

简陋射鸡游戏

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

目录

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崩溃了很多次!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值