前言
这次的作业个人感觉挺好玩的,我给做成了射击游戏。同时我也在其中学到了unity的一些有趣应用,
下面先把大的框架给讲一下,然后再着重讲一下我个人的收获吧。
一、任务要求
游戏规则(自定义)
控制你的超进化版鼠标枪射击飞行玩偶(飞碟),击中玩偶或者放任玩偶下落都会有相应的得分,得分如表。游戏共十回合,随回合数增加,玩偶数跟飞行速度增加。若得分为负,游戏失败;若回合进行完毕,游戏胜利。
飞碟 | 击中 | 掉落 |
---|---|---|
蝙蝠 | 5 | -2 |
恶鬼 | 10 | -2 |
软泥怪 | 20 | -2 |
兔子 | -10 | 5 |
二、 项目展示
这里不得不提一下,我特别喜欢作业三的mvc框架,只需三个文件(Model、Controllor、UserGUI)就可以将结构讲明白。所以我这次也是在那个框架的基础上进行修改的。
1.mvc架构
Model 模型(其实把大部分类都给汇总了)
这里的导演类等都是以前作业内容就不再赘述
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace mygame
{
public interface ISceneController //加载场景
public interface IUserAction //用户互动会发生的事件
public class SSDirector : System.Object//导演类
public class ScoreController : System.Object{//分数控制器
public class DiskFactory : System.Object {//飞碟工厂
public class DiskModel//飞碟模型
public class Diskbehaviour : MonoBehaviour//飞碟行为
public class GunModel//枪模型
public class Gunbehaviour: MonoBehaviour//枪行为
}
View 交互界面
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using mygame;
public class UserGUI : MonoBehaviour {
private IUserAction action;
public int sign = 0;
bool isShow = false;
void Start()
{
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
}
void OnGUI()
{
//规则展示
if (GUI.Button(new Rect(10, 10, 60, 30), "Rule", new GUIStyle("button")))
{
if (isShow)
isShow = false;
else
isShow = true;
}
if(isShow)
{
GUI.Label(new Rect(Screen.width / 2 - 85, 10, 200, 50), "击败恶魔,保护小兔子");
GUI.Label(new Rect(Screen.width / 2 - 150, 30, 350, 50), "子弹打中恶魔得分,让恶魔逃跑丢分,打中小兔子丢分");
GUI.Label(new Rect(Screen.width / 2 - 85, 50, 250, 50), "点击屏幕进行射击");
}
//游戏准备
if (sign == 0)
{
if (GUI.Button (new Rect (Screen.width / 2 - 80, Screen.height / 2, 160, 20), "开始游戏")){
action.gamestart();
}
}
//游戏中
else if(sign == 1){
string say = "当前回合回合数为" + action.getround().ToString() +
",分数为" + ScoreController.GetScoreController().getscore().ToString() +"分";
GUI.Box (new Rect (Screen.width / 2 - 100, 70, 200,30), say);
}
//游戏结束
else if (sign == 2||sign == 3)
{
string say;
if(sign == 2)say = "你输了,一共坚持了" + action.getround().ToString() + "局";
else say = "你赢了,一共得了" + ScoreController.GetScoreController().getscore().ToString() +"分";
GUI.Box (new Rect (Screen.width / 2 - 100, Screen.height / 2 + 50, 200, 30), say);
if (GUI.Button (new Rect (Screen.width / 2 - 80, Screen.height / 2, 160, 20), "重开")){
action.gamestart();
sign = 1;
}
}
}
}
场记 Controllor
其实这里再写一个游戏控制的类,或者把游戏规则丢进分数控制器里应该会好一点,但是本人有点懒,就把场景控制全丢这了
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using mygame;
public class Controllor : MonoBehaviour, ISceneController, IUserAction
{
public static System.Random rand;
UserGUI user_gui;
DiskFactory diskFactory;
ScoreController scoreController;
List<DiskModel> diskModels;
public int round;
GunModel gun;
void Start ()
{
//设置导演
SSDirector director = SSDirector.GetInstance();
//设置场景
director.CurrentScenceController = this;
//新建View
user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
//设置分数控制器
scoreController = ScoreController.GetScoreController();
//设置飞碟工厂
diskFactory =DiskFactory.getFactory();
//设置飞碟模型
diskModels = diskFactory.GetDiskModels();
//设置随机种子
rand = new System.Random((int) System.DateTime.Now.Ticks & 0x0000FFFF);
LoadResources();
}
这里稍微讲一下上面倒数第二行。这个随机搞了我半天,因为这里的C#调用了unity的库,会出现重载等情况,
导致一些常用的类诸如DateTime、Random不能跟平常一样使用。这时候我们可以通过在类前面添加System来进
行调用。
然后在使用C#随机数时存在伪随机数的情况,这里用时间做种子避免了。然后unity本身也是用random的,不过
用法跟原来得就不一样了,有无伪随机我没有试过。
最后,下面的代码无缝衔接上文
public void LoadResources()
{
gun = new GunModel();
}
public void gamestart(){
round = 0;
scoreController.gamestart();
gun.gamestart();
user_gui.sign = 1;
}
public void gamestop(){
diskFactory.recycleall();
gun.gameend();
}
void Update(){
if(user_gui.sign==1){
checkdisk();
//检查是否分数为负数
user_gui.sign = Check();
if(user_gui.sign==1&&diskModels.Count==0){
//当前回合结束,增加回合并发射飞碟
round++;
if(round<=10)sentdisk();
}
//检查是否超过十局
user_gui.sign = Check();
if(user_gui.sign!=1)gamestop();
}
}
public void checkdisk(){
for(int i = diskModels.Count-1;i>=0;i--){
if(diskModels[i].status()==0)continue;
string effect;
if(diskModels[i].status()==1){//掉出空间
scoreController.addscore(diskModels[i].getdiegrade());
effect = "disappear";
}
else{//被打中
scoreController.addscore(diskModels[i].getgrade());
if(diskModels[i].getkind()==3)effect = "death";
else effect = "ok";
}
//网上下好的特效预设
Destroy(Object.Instantiate(Resources.Load(effect, typeof(GameObject)),diskModels[i].getpos(),Quaternion.identity) as GameObject,3);
//回收模型
diskModels[i].destroy();
DiskFactory.getFactory().recycleDisk(i);
}
}
public void sentdisk(){
int num = round/3+1;//飞碟数量
int speed = round/2+1;//飞碟速度
diskFactory.prepareDisks(num,rand);
for(int i = 0;i< diskModels.Count;i++){
diskModels[i].setdisk(speed,rand);
}
}
public int Check()
{
//检测游戏状态
if(scoreController.getscore()<0)return 2;
if(round>10)return 3;
return 1;
}
public int getround(){
return round;
}
}
2.分数控制器、飞碟工厂和飞碟模型
ScoreController 分数控制器
public class ScoreController : System.Object{//分数控制器
private static ScoreController scoreController;
int score;
public static ScoreController GetScoreController () {
if (scoreController == null) {
scoreController = new ScoreController ();
}
return scoreController;
}
public int getscore(){
return score;
}
public void addscore(int point){
score += point;
}
public void gamestart(){
score = 0;
}
}
DiskFactory 飞碟工厂
这里的实现是参考学长博客的,个人觉得把基本的逻辑都实现了,但不是很清楚具体实现是不是这样。
public class DiskFactory : System.Object {//飞碟工厂
public DiskModel diskPrefab;
private static DiskFactory diskFactory;
List<DiskModel> usingDisks;
List<DiskModel>[] uselessDisks;
public static DiskFactory getFactory () {
if (diskFactory == null) {
diskFactory = new DiskFactory ();
diskFactory.uselessDisks = new List<DiskModel>[4];
diskFactory.usingDisks = new List<DiskModel>();
for(int i = 0;i<4;i++){
diskFactory.uselessDisks[i] = new List<DiskModel>();
}
}
return diskFactory;
}
public void prepareDisks (int diskCount,System.Random r) {
for (int i = 0; i < diskCount; i++) {
int kind = r.Next()%4;//飞碟种类随机
if (uselessDisks[kind].Count == 0) {
DiskModel disk = new DiskModel(kind);
usingDisks.Add (disk);
} else {
DiskModel disk = uselessDisks[kind][0];
uselessDisks[kind].RemoveAt (0);
usingDisks.Add (disk);
}
}
}
public void recycleall(){
for(int i = usingDisks.Count-1;i>=0;i--)recycleDisk(i);
}
public void recycleDisk (int index) {
uselessDisks[usingDisks[index].getkind()].Add (usingDisks[index]);
usingDisks.RemoveAt (index);
}
public List<DiskModel> GetDiskModels(){
return usingDisks;
}
}
DiskModel 飞碟模型
这里首先开了个静态数组,存储了累死累活测出来的可行的位置、施加力、扭矩力对应数组。
public class DiskModel{
GameObject disk;
static Vector3[] position = {new Vector3(-20,10,20),new Vector3(15,5,10),new Vector3(-20,0,0),new Vector3(20,20,15),new Vector3(0,0,10)};
static Vector3[] force = {new Vector3(5,5,-4),new Vector3(-8,5,0),new Vector3(8,8,5),new Vector3(-5,0,-1.5f),new Vector3(0,8,0)};
static Vector3[] rota = {new Vector3(5,5,-5),new Vector3(10,0,1),new Vector3(-3,5,4),new Vector3(1,-12,-8),new Vector3(-3,-6,4)};
int grade;
int diegrade;
int kind;
public int statu;
Diskbehaviour diskbehaviour;
public DiskModel(int k){
kind = k;
string kstr = "disk";
kstr += kind.ToString();
disk = Object.Instantiate(Resources.Load(kstr, typeof(GameObject))) as GameObject;
if(kind== 0){
grade = 5;
diegrade = -2;
}
else if(kind == 1){
grade = 10;
diegrade = -2;
}
else if(kind == 2){
grade = 20;
diegrade = -2;
}
else if(kind == 3){
grade = -10;
diegrade = 5;
}
statu = 0;
diskbehaviour = disk.AddComponent(typeof(Diskbehaviour)) as Diskbehaviour;
diskbehaviour.setdisk(this);
}
这里对飞碟运动的处理是我这次很受益的地方,本来我已经写好了自制的move类了,但是因为数据的原因,会显得很僵硬。然后回头一看学长博客,只能说这个力的运用秒不可言。其中扭矩力的添加,单纯是我觉得物体转起来会更3D一些
public void setdisk(int sp,System.Random r){//改变飞碟速度
int rp = r.Next()%position.GetLength(0);
disk.transform.position = position[rp];
Rigidbody rigidbody;
rigidbody = disk.GetComponent<Rigidbody>();
//启动刚体
rigidbody.WakeUp();
rigidbody.useGravity = true;
//添加瞬间力
float disksp = 1;
for(int i = 1;i<sp;i++)disksp *= 1.1f;
rigidbody.AddForce(force[rp]*Random.Range(5, 8)*disksp/5, ForceMode.Impulse);
//添加旋转力
rigidbody.AddTorque(rota[rp] * 10);
}
public int getgrade(){
return grade;
}
public int getdiegrade(){
return diegrade;
}
public int getkind(){
return kind;
}
public Vector3 getpos(){
return disk.transform.position;
}
public int status(){
if(disk.transform.position.y<-3)statu = 1;
return statu;
}
public void destroy(){
statu = 0;
disk.GetComponent<Rigidbody>().Sleep();
disk.GetComponent<Rigidbody>().useGravity = false;
disk.transform.position = new Vector3(0f, -99f, 0f);
}
}
最后这里对飞碟刚体的处理更让我大为叹服,把物体的刚体运算关掉,再放到看不到的角落,确实是个好方法
3.枪与子弹的实现
这一部分,我学会了如何让枪口随鼠标移动,子弹发射,与子弹碰撞这三点。个人觉得这应该是游戏制作中挺还是实用的。
GunModel 枪模型
public class GunModel{
GameObject gun;
Gunbehaviour gunbehaviour;
public GunModel(){
gun = Object.Instantiate(Resources.Load("gun", typeof(GameObject)),new Vector3(1,0,-9),Quaternion.identity) as GameObject;
gunbehaviour = gun.AddComponent(typeof(Gunbehaviour)) as Gunbehaviour;
}
public void gamestart(){
gunbehaviour.setaction(1);
}
public void gameend(){
gunbehaviour.setaction(0);
gun.transform.rotation = Quaternion.identity;
}
}
Gunbehaviour 枪行为
public class Gunbehaviour: MonoBehaviour{
public Vector3 mousePos;
int action = 0;
float firesp = 100;
GameObject bullet;
GameObject fire;
void Start(){
bullet = Object.Instantiate(Resources.Load("bullet", typeof(GameObject)),new Vector3(0f, -99f, -99f),Quaternion.identity) as GameObject;
bullet.GetComponent<Rigidbody>().useGravity = false;
fire = transform.GetChild(1).gameObject;
}
技巧一 :通过脚本来找到子对象
获得对象的transform后可以通过GetChild(index)来找到对应下标的子对象,transform的gameobject可以直接找到对应对象。
void Update(){
if(action==1){
//枪支移动
Vector3 offset = mousePos - Input.mousePosition;
transform.Rotate(Vector3.up * offset.x *-0.1f, Space.World);//左右旋转
transform.Rotate(Vector3.right * -offset.y *-0.1f, Space.World);//上下旋转
mousePos = Input.mousePosition;
//射击
if(Input.GetMouseButtonDown(0)){
GameObject newbullet = Instantiate(bullet,fire.transform.position,transform.rotation);
Destroy(newbullet, 3.0f);
Rigidbody clone = newbullet.GetComponent<Rigidbody>();
clone.velocity = transform.TransformDirection(Vector3.forward*firesp);
}
}
}
public void setaction(int action){
this.action = action;
if(action == 1)mousePos = new Vector3(Screen.width / 2,Screen.height/2-10,0);
}
}
技巧二 鼠标移动调动物体旋转 :
这里我们可以通过Input.mousePosition来不断获得鼠标焦点位置,而通过记录鼠标位置的变化,我们可以通过同步移动物体的rotation来实现调动旋转的效果。其中-0.1f相当于灵敏度与方向,可自行调整
技巧三 子弹射击 :
首先我们在枪口这里设置好空对象,这样我们就可以通过找到预设子对象的位置来找到枪口。
然后是transform.TransformDirection,这个方法是将局部坐标转为世界坐标,说人话就是你走路只管前后左右(你当前rotation的相对坐标),这个方法会自动帮你转成东南西北(屏幕里看到的世界坐标,即绝对坐标)
子弹碰撞实现
两者使用碰撞体
其中实现碰撞(要添加脚本的)勾选Is Trigger
脚本实现的函数
添加脚本如下
public class Diskbehaviour : MonoBehaviour{
DiskModel disk;
public void setdisk(DiskModel disk){
this.disk = disk;
}
void OnTriggerEnter(Collider collider) {
if(collider.tag == "Finish"){
disk.statu = 2;
}
}
}
其中我们需要修改被碰撞(没有脚本)的游戏对象的tag,如下
三、 展示效果
1.展示图
2.展示视频
枪打恶鬼 演示