简陋射鸡游戏
本来老师的意思应该是做一个保龄球类的游戏(扔石头砸一窝鸡),然而由于我渣渣的阅读理解,将错就错做成了射击游戏。
这个界面真是蠢蠢的,而且还是俯视图。做成俯视图主要是我发现想打中不容易,所以调整了摄像机。
目录
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崩溃了很多次!
3026

被折叠的 条评论
为什么被折叠?



