规则
鼠标点击飞碟,即可获得分数,不同飞碟分数不一样,飞碟的初始位置与飞行速度随机,随着分数增加,游戏难度增加。初始时每个玩家都有6条生命,漏打飞碟扣除一条生命,直到生命为0游戏结束。
要求:
Singleton
模板类扩展:
FlyActionManager
飞碟飞行的动作管理类,当场景控制器需要飞碟飞行的时候,调用动作管理类的方法,让飞碟飞行
public class FlyActionManager : SSActionManager
{
public UFOFlyAction fly; //飞碟飞行的动作
public FirstController scene_controller; //当前场景的场景控制器
protected void Start()
{
scene_controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
scene_controller.action_manager = this;
}
//飞碟飞行
public void UFOFly(GameObject disk, float angle, float power)
{
fly = UFOFlyAction.GetSSAction(disk.GetComponent().direction, angle, power);
this.RunAction(disk, fly, this);
}
}
UFOFlyAction
给飞碟一个方向和一个力,然后飞碟模拟做有向下加速度的飞行动作,直到飞碟不在相机范围内,就停止动作。可以设定飞碟位置y方向的值,当它小于多少的时候,不再飞行,然后等待场景控制器和飞碟工厂进行配合回收飞碟。其实可以使用
Rigidbody
组件,开启Unity的物理模拟效果。
public class UFOFlyAction : SSAction
{
public float gravity = -5; //向下的加速度
private Vector3 start_vector; //初速度向量
private Vector3 gravity_vector = Vector3.zero; //加速度的向量,初始时为0
private float time; //已经过去的时间
private Vector3 current_angle = Vector3.zero; //当前时间的欧拉角
private UFOFlyAction() { }
public static UFOFlyAction GetSSAction(Vector3 direction, float angle, float power)
{
//初始化物体将要运动的初速度向量
UFOFlyAction action = CreateInstance();
if (direction.x == -1)
{
action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power;
}
else
{
action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;
}
return action;
}
public override void Update()
{
//计算物体的向下的速度,v=at
time += Time.fixedDeltaTime;
gravity_vector.y = gravity * time;
//位移模拟
transform.position += (start_vector + gravity_vector) * Time.fixedDeltaTime;
current_angle.z = Mathf.Atan((start_vector.y + gravity_vector.y) / start_vector.x) * Mathf.Rad2Deg;
transform.eulerAngles = current_angle;
//如果物体y坐标小于-10,动作就做完了
if (this.transform.position.y < -10)
{
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
使用序列化的方法,将DiskData的属性显示在Inspector中。需要我们新建一个脚本,并且继承Editor,在类的前面添加:
[CustomEditor(typeof(DiskData))]
,这里的DiskData就是要实现自定义组件的类。在这之后添加[CanEditMultipleObjects]
,实现了多个对象可以不同的修改。如果没有这个标签,那么在Inspector修改之后,拥有这个DiskData作为组件的预制体所有修改都会同步。SerializedProperty
是我们需要序列化的属性,通过EditorGUILayout
的不同的方法,可以在Inspector中用不同方式呈现我们序列化的属性。序列化的属性的呈现方式需要在OnInspectorGUI
中进行编写。
//脚本DiskData
public class DiskData : MonoBehaviour
{
public int score = 1; //射击此飞碟得分
public Color color = Color.white; //飞碟颜色
public Vector3 direction; //飞碟初始的位置
public Vector3 scale = new Vector3( 1 ,0.25f, 1); //飞碟大小
}
//脚本MyDiskEditor
[CustomEditor(typeof(DiskData))]
[CanEditMultipleObjects]
public class MyDiskEditor: Editor
{
SerializedProperty score; //分数
SerializedProperty color; //颜色
SerializedProperty scale; //大小
void OnEnable()
{
//序列化对象后获得各个值
score = serializedObject.FindProperty("score");
color = serializedObject.FindProperty("color");
scale = serializedObject.FindProperty("scale");
}
public override void OnInspectorGUI()
{
//更新serializedProperty,始终在OnInspectorGUI的开头执行此操作
serializedObject.Update();
//设置滑动条
EditorGUILayout.IntSlider(score, 0, 5, new GUIContent("score"));
if (!score.hasMultipleDifferentValues)
{
//显示进度条
ProgressBar(score.intValue / 5f, "score");
}
//显示值
EditorGUILayout.PropertyField(color);
EditorGUILayout.PropertyField(scale);
//将更改应用于serializedProperty,始终在OnInspectorGUI的末尾执行此操作
serializedObject.ApplyModifiedProperties();
}
private void ProgressBar(float value, string label)
{
Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
EditorGUI.ProgressBar(rect, value, label);
//中间留一个空行
EditorGUILayout.Space();
}
}
如果自定义组件只是运用在一个预制体上,也可以使用其他方法简化代码,具体操作请看官方文档。我设计了三种飞碟,把DiskData脚本挂载在每个飞碟预制体上就可以对它们进行设置了,最后实现效果如下图。
飞碟工厂类,根据回合的不同实现随机发送不同的飞碟。在场景控制器需要某种飞碟的时候,飞碟工厂从仓库(
List
)中获取这种飞碟,如果仓库中没有,则新的实例化一个飞碟,然后添加到正在使用的飞碟列表中。当场景控制器发现飞碟被打中或者飞碟掉出摄像机视野外,将执行回收飞碟。(ps: 这里也留了一个问题为啥List中保存DiskData,而不直接保存GameObject,我觉得是DiskData可以用gameObject属性直接得到GameObject对象,而GameObject对象要用free GetComponent
方法去查找DiskData组件,而GetComponent<>()比直接获取属性慢的多,使用DiskData频率可能高于直接使用GameObject对象,所以优选保存DiskData)。。。然而获取飞碟的时候我返回了GameObject,而且在场景控制器我也是保存的GameObject :)()
public class DiskFactory : MonoBehaviour
{
public GameObject disk_prefab = null; //飞碟预制体
private List used = new List(); //正在被使用的飞碟列表
private List free = new List(); //空闲的飞碟列表
public GameObject GetDisk(int round)
{
int choice = 0;
int scope1 = 1, scope2 = 4, scope3 = 7; //随机的范围
float start_y = -10f; //刚实例化时的飞碟的竖直位置
string tag;
disk_prefab = null;
//根据回合,随机选择要飞出的飞碟
if (round == 1)
{
choice = Random.Range(0, scope1);
}
else if(round == 2)
{
choice = Random.Range(0, scope2);
}
else
{
choice = Random.Range(0, scope3);
}
//将要选择的飞碟的tag
if(choice <= scope1)
{
tag = "disk1";
}
else if(choice <= scope2 && choice > scope1)
{
tag = "disk2";
}
else
{
tag = "disk3";
}
//寻找相同tag的空闲飞碟
for(int i=0;iif(free[i].tag == tag)
{
disk_prefab = free[i].gameObject;
free.Remove(free[i]);
break;
}
}
//如果空闲列表中没有,则重新实例化飞碟
if(disk_prefab == null)
{
if (tag == "disk1")
{
disk_prefab = Instantiate(Resources.Load("Prefabs/disk1"), new Vector3(0, start_y, 0), Quaternion.identity);
}
else if (tag == "disk2")
{
disk_prefab = Instantiate(Resources.Load("Prefabs/disk2"), new Vector3(0, start_y, 0), Quaternion.identity);
}
else
{
disk_prefab = Instantiate(Resources.Load("Prefabs/disk3"), new Vector3(0, start_y, 0), Quaternion.identity);
}
//给新实例化的飞碟赋予其他属性
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
disk_prefab.GetComponent().material.color = disk_prefab.GetComponent().color;
disk_prefab.GetComponent().direction = new Vector3(ran_x, start_y, 0);
disk_prefab.transform.localScale = disk_prefab.GetComponent().scale;
}
//添加到使用列表中
used.Add(disk_prefab.GetComponent());
return disk_prefab;
}
//回收飞碟
public void FreeDisk(GameObject disk)
{
for(int i = 0;i < used.Count; i++)
{
if (disk.GetInstanceID() == used[i].gameObject.GetInstanceID())
{
used[i].gameObject.SetActive(false);
free.Add(used[i]);
used.Remove(used[i]);
break;
}
}
}
}
FirstController
游戏有三种状态,游戏开始,游戏中,游戏结束。(最好用枚举吧,我用了三个bool变量…)。游戏开始:设置一个定时器,定时从工厂那里获取飞碟,并且发送飞碟,一次只拿一个并立即发送。(有看过师兄的代码是一次就从工厂拿多个,然后慢慢发送飞碟,如果飞碟完了又一次性拿多个)。游戏中:根据定时器,时间到了就获取飞碟并发送。当分数到达10分,将会多增加一种飞碟随机发送这两种并且缩短发送间隔,达到25分则三种飞碟随机发送,发送概率不一样。游戏结束:显示最高分,提供重新开始按钮。
用户点击屏幕发送射线
如果打中则激活爆炸粒子效果,一段时间后,飞碟工厂再回收。这里使用了协程的概念,我觉得和多线程的意思很像,
StartCoroutine
开启一个协程,StartCoroutine
后面的代码和新的协程一起执行,使用yield
暂停协程的执行,yield return
的值是代表什么时候继续协程的执行,这样在yield return
后面的代码将延迟一点时间执行。更详细的解释请看官方文档。(ps: 这里有一点迷的就是,加上协程后,用户点击一次屏幕会执行两次Hit函数,所以我用了判断物体是否被打中,打中了就不要再继续执行后面代码)
发送飞碟
从飞碟队列中取出一个飞碟,然后重新设置它的位置(因为拿到的可能是使用过的飞碟)。发送的时候检测未射中的飞碟列表,是否已经飞出镜头外了,如果是用户减一条生命。这里直接调用了user_gui的方法,其实有点违背了MVC,所以如果要改进可以让计分员记录生命,这样可以合并为一个场景中专门记录数值的类。
部分代码如下:
void Start ()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
disk_factory = Singleton.Instance;
score_recorder = Singleton.Instance;
action_manager = gameObject.AddComponent() as FlyActionManager;
user_gui = gameObject.AddComponent() as UserGUI;
}
//发射射线
public void Hit(Vector3 pos)
{
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
bool not_hit = false;
for (int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
//射线打中物体
if (hit.collider.gameObject.GetComponent() != null)
{
//射中的物体要在没有打中的飞碟列表中
for (int j = 0; j < disk_notshot.Count; j++)
{
if (hit.collider.gameObject.GetInstanceID() == disk_notshot[j].gameObject.GetInstanceID())
{
not_hit = true;
}
}
if(!not_hit)
{
return;
}
disk_notshot.Remove(hit.collider.gameObject);
//记分员记录分数
score_recorder.Record(hit.collider.gameObject);
//显示爆炸粒子效果
Transform explode = hit.collider.gameObject.transform.GetChild(0);
explode.GetComponent().Play();
//等0.08秒后执行回收飞碟
StartCoroutine(WaitingParticle(0.08f, hit, disk_factory, hit.collider.gameObject));
break;
}
}
}
//暂停几秒后回收飞碟
IEnumerator WaitingParticle(float wait_time, RaycastHit hit, DiskFactory disk_factory, GameObject obj)
{
yield return new WaitForSeconds(wait_time);
//等待之后执行的动作
hit.collider.gameObject.transform.position = new Vector3(0, -9, 0);
disk_factory.FreeDisk(obj);
}
//发送飞碟
private void SendDisk()
{
float position_x = 16;
if (disk_queue.Count != 0)
{
GameObject disk = disk_queue.Dequeue();
disk_notshot.Add(disk);
disk.SetActive(true);
//设置被隐藏了或是新建的飞碟的位置
float ran_y = Random.Range(1f, 4f);
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
disk.GetComponent().direction = new Vector3(ran_x, ran_y, 0);
Vector3 position = new Vector3(-disk.GetComponent().direction.x * position_x, ran_y, 0);
disk.transform.position = position;
//设置飞碟初始所受的力和角度
float power = Random.Range(10f, 15f);
float angle = Random.Range(15f, 28f);
action_manager.UFOFly(disk,angle,power);
}
for (int i = 0; i < disk_notshot.Count; i++)
{
GameObject temp = disk_notshot[i];
//飞碟飞出摄像机视野也没被打中
if (temp.transform.position.y < -10 && temp.gameObject.activeSelf == true)
{
disk_factory.FreeDisk(disk_notshot[i]);
disk_notshot.Remove(disk_notshot[i]);
//玩家血量-1
user_gui.ReduceBlood();
}
}
}
一般射击都是从点击处射出子弹,带有碰撞器,当碰撞器与飞碟碰撞器触碰时候,应该是开启子弹上的粒子效果才对,然后播放完毕后子弹消失。
记录分数和重置分数
public class ScoreRecorder : MonoBehaviour
{
public int score; //分数
void Start ()
{
score = 0;
}
//记录分数
public void Record(GameObject disk)
{
int temp = disk.GetComponent().score;
score = temp + score;
//Debug.Log(score);
}
//重置分数
public void Reset()
{
score = 0;
}
}
小结
这次游戏使用工厂对象实现了预制体实例化后的重用,提高了游戏性能,还加入了获取鼠标输入,增加了与用户的交互。在写博客的过程中发现了很多代码中存在的问题,也发现了许多游戏中需要改进的地方。
完整项目请点击传送门,Assets/Scenes/中的myScene是本次游戏场景