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崩溃了很多次!
- lab6
- ucore lab6
- ucore lab6
- lab6 mallocbNote
- 操作系统实验报告 lab6
- [NA]Lab6:正交多项式拟合
- csapp-lab6 malloc-lab
- SJTU->SE->ICS->LAB6 Malloc
- 操作系统ucore lab6实验报告
- 操作系统ucore lab6实验报告
- MIT6.828 LAB6: Network Driver
- Lab6 uC/OS II(windows平台)
- ucore操作系统lab6(理论部分)
- ucore操作系统lab6 —— 调度算法
- 嵌入式lab6——Linux系统调用
- MIT 分布式系统 实验 yfs 6.824 2012 LAB6
- Mit 分布式系统导论,Distributed Systems ,lab1 -lab6 总结,实验一到实验六总结
- <csapp> malloc lab (《深入理解计算机系统》lab6) (附lab4\lab5下载地址)
- 安装五笔输入法,并调出
- Stack overflow
- centos7安装jdk8
- 我希望进入大学时就能知道的一些事儿
- SVN安装配置与使用
- lab6
- Android Studio常用快捷键
- python文件IO相关
- windows卸载程序需要密码/防卸载功能
- 八皇后问题(dfs常规解法)
- hdu1048 The Hardest Problem Ever
- VS2013解决rror C4996: 'fopen': This function or variable may be unsafe. Consider using fopen_s instead
- 【网络】HTTP2.0新特性
- 关于解决Eclipse报错显示Unable to build: the file dx.jar was not loaded from the SDK folder