改进飞碟(Hit UFO)游戏
游戏内容要求
- 按adapter模式设计图修改飞碟游戏
- 使它同时支持物理运动与运动学(交换)运动
编程实践
本次作业直接在上一次打飞碟游戏的基础上增加adapter设计模式(增加一个内含PhysisActionManager
和CCActionManager
接口的ActionManagers
类),提供一个运动的统一接口,根据玩家的具体操作再决定是调用物理运动类还是运动学动作管理器类。也就是游戏同时存在两个飞碟运动模式,支持物理运动和运动学互相切换。
- Adapter(适配器)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。做法是将类自己的接口包裹在一个已存在的类中。有时也称为包装样式/包装。
本次作业采用的Adapter模式属于对象适配器模式,即adapter类持有不同的动作管理器对象,并实现对应的想提供的目标接口。
本次游戏的结构UML图如下:
玩家切换运动模式的方法是在游戏界面点击鼠标右键,但并不会马上中断游戏切换运动模式,而是在下一个trail开始时进行切换。
动作接口
首先在原来的SceneController
类中再定义一个统一的动作接口(模板类),包含了物理运动和运动学动作管理器类都需要实现的方法addForceOnUFO(GameObject UFO)
,适配器类Adapter继承该接口。
public interface IActionManager
{
void addForceOnUFO(GameObject UFO);
}
用户接口
当用户接口类UserInterface
中的Update()
方法检测到鼠标右键被按下时,调用方法切换动作管理器。修改后的UserInterface
类Update()
方法如下:
// Update is called once per frame
void Update () {
//检测鼠标点击(左键)
if (Input.GetMouseButtonDown(0))
{
Vector3 mousePos = Input.mousePosition;
action.hitUFO(mousePos);
}
//检测用户按下空格
if (Input.GetKeyDown(KeyCode.Space))
{
action.launchUFO();
}
//检测鼠标右键点击
if (Input.GetButtonDown("Fire2"))
{
action.switchActionModeNextRound();
}
}
相应地,在SceneController类的IUserAction接口中增加该switchActionInNextRound()
方法的声明:
public interface IUserAction
{
void launchUFO();
void hitUFO(Vector3 mousePos);
void switchActionModeNextRound();
}
适配器类
接着编写最重要的适配器类UFOActionAdapter
,该类需要实现IActionManager
接口(实现不同模式的addForceOnUFO(GameObject UFO)
方法的调用),并且持有PhysisActionManager
和CCActionManager
对象,用一个整数型成员变量WhichActionManager
来记录当前使用的是哪一个动作管理器,值为0表示当前处于物理运动模式,值为1表示当前处于运动学动作管理器模式,只需要WhichActionManager = 1-WhichActionManager
就能实现切换运动模式。代码如下:
public class UFOActionAdapter : IActionManager
{
private PhysisActionManager physisActionManager;
private CCActionManager ccActionManager;
int whichActionManager = 0; // 0 -> PhysisActionManager, 1 -> CCActionManager
public UFOActionAdapter()
{
physisActionManager = PhysisActionManager.getInstance();
ccActionManager = CCActionManager.getInstance();
}
public void switchActionMode()
{
whichActionManager = 1 - whichActionManager;
}
public void addForceOnUFO(GameObject UFO)
{
if(whichActionManager == 0)
{
physisActionManager.addForce(UFO);
}
else
{
ccActionManager.addForce(UFO);
}
}
}
在SceneController中使用bool型成员变量switchActionManager来标记是否需要切换运动模式,初始化为false,表示初始不做模式切换时游戏处于物理运动模式。代码如下:
private bool switchActionManager = false;
如果switchActionModeNextRound()
方法被调用,则将switchActionManager
置为true:
public void switchActionModeNextRound()
{
myStatusCtrl.showSwitchText();
switchActionManager = true;
}
并且在每一轮trail开始时检测switchActionManager
是否为true,若为true则切换运动模式并将switchActionManager
重置为false:
public void launchUFO()
{
//每次发射之前清0分数
if (switchActionManager)
{
switchActionManager = false;
myUFOCtrl.switchActionManager();
}
myStatusCtrl.resetScore();
myUFOCtrl.launchUFO();
}
物理运动管理器类
定义物理运动管理器,主要实现给游戏对象添加一个xyz轴方向上都是在一定数值范围内随机生成的瞬时力,代码如下:
public class PhysisActionManager
{
private SceneController scene;
private static PhysisActionManager instance;
private PhysisActionManager()
{
scene = SceneController.getInstance();
}
public static PhysisActionManager getInstance()
{
if (instance == null) instance = new PhysisActionManager();
return instance;
}
private Vector3 getRandomForce()
{
int x = UnityEngine.Random.Range(-30, 31);
int y = UnityEngine.Random.Range(30, 41);
int z = UnityEngine.Random.Range(20, 31);
float t = 0.7f + scene.getTrailNum() / 20;
return new Vector3(x, y, z) * t;
}
public void addForce(GameObject UFO)
{
Vector3 force = getRandomForce();
UFO.GetComponent<Rigidbody>().useGravity = true;
UFO.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse);
}
}
运动学运动管理器
运动学管理器模式下的物体不使用重力属性,即把游戏对象的刚体属性useGravity
设为false,并且给刚体一个初速度使其沿某个方向运动,代码如下:
public class CCActionManager
{
private SceneController scene;
private static CCActionManager instance;
private CCActionManager()
{
scene = SceneController.getInstance();
}
public static CCActionManager getInstance()
{
if (instance == null) instance = new CCActionManager();
return instance;
}
public void addForce(GameObject UFO)
{
// 控制飞碟向某个随机方向持续移动
UFO.GetComponent<Rigidbody>().useGravity = false;
UFO.GetComponent<Rigidbody>().velocity = new Vector3(5, 15, 5);
}
}
飞碟控制类
飞碟控制类UFOController
中需要定义一个适配器成员对象,用来在launchUFOs(roundNum)
函数里(发射飞碟时)调用不同模式下的addForceOnUFO(GameObject)
方法。修改后的UFOController类如下:
using CarolSum.com;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UFOController : MonoBehaviour {
//预设文件
public GameObject PlaneItem, LauncherItem, ExplosionItem;
public Material greenMat, redMat, blueMat;
private GameObject plane, launcher, explosion;
private SceneController scene;
private UFOActionAdapter adapter;
//发射每一个飞碟的时间间隔
private const float LAUNCH_GAP = 0.1f;
// Use this for initialization
void Start () {
scene = SceneController.getInstance();
scene.setUFOController(this);
plane = Instantiate(PlaneItem);
launcher = Instantiate(LauncherItem);
explosion = Instantiate(ExplosionItem);
adapter = new UFOActionAdapter();
}
// Update is called once per frame
void Update () {
UFOFactory.getInstance().detectLandingUFOs();
}
public void launchUFO()
{
int trailNum = scene.getTrailNum() > 3 ? 3 : scene.getTrailNum();
Debug.Log("发射!");
if (!UFOFactory.getInstance().isLaunching())
{
StartCoroutine(launchUFOs(trailNum));
}
}
IEnumerator launchUFOs(int roundNum)
{
for(int i = 0; i<roundNum; i++)
{
GameObject UFO = UFOFactory.getInstance().getUFO();
UFO.transform.position = launcher.transform.position;
UFO.GetComponent<MeshRenderer>().material = getMaterial(scene.getTrailNum());
adapter.addForceOnUFO(UFO);
//每隔LAUNCH_GAP时间发射一个UFO
yield return new WaitForSeconds(LAUNCH_GAP);
}
}
public void switchActionManager()
{
adapter.switchActionMode();
}
public void hitUFO(Vector3 mousePos)
{
Ray ray = Camera.main.ScreenPointToRay(mousePos);
RaycastHit hit;
if(Physics.Raycast(ray, out hit))
{
if (hit.collider.gameObject.tag.Equals("UFO"))
{
createExplosion(hit.collider.gameObject.transform.position);
scene.addScore();
UFOFactory.getInstance().RecyclingUFO(hit.collider.gameObject);
}
}
}
private void createExplosion(Vector3 position)
{
explosion.transform.position = position;
explosion.GetComponent<ParticleSystem>().GetComponent<Renderer>().material = getMaterial(scene.getTrailNum());
explosion.GetComponent<ParticleSystem>().Play();
}
private Material getMaterial(int roundNum)
{
switch(roundNum % 3)
{
case 0:
return redMat;
case 1:
return greenMat;
case 2:
return blueMat;
default:
return redMat;
}
}
}
游戏各预制的属性设置如下:
Canvas:
Explosion:
Launcher:
Plane:
UFO:
Main Camera挂载:
Empty对象挂载:
最终运行效果如下: