1、改进飞碟(Hit UFO)游戏:
游戏内容要求:
·按 adapter模式 设计图修改飞碟游戏
·使它同时支持物理运动与运动学(变换)运动
本次作业是基于上次的飞碟游戏改进的,这里是上次作业的博客链接。
1.1适配器模式
适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。
适配器模式的意图:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
适配器模式的UML图如下:
在我的理解中,本次打飞碟作业之所以需要使用适配器模式,是因为我们需要在FirstController类中调用飞碟的飞行动作,即FirstController需要使用现有的飞行动作类(DiskFlyAction),但是飞行动作类的接口不能直接被FirstController调用,因为它并不挂载在飞碟上,也就是说不能通过飞碟实例简单地来调用该飞碟的飞行动作。
所以,我们需要使用适配器模式,将DiskFlyAction接口转化成我们能够直接在FirstController中调用的形式——用ActionManager类来完成转化。对应到上面的UML图,ActionManager充当Target,是FirstController可以直接访问的接口;DiskFlyAction是我们希望使用、但是由于接口不匹配无法直接使用的Adaptee。
体现在代码中:
FirstController:
//FirstController
public FlyActionManager action_manager;//可以直接访问的接口
public DiskFactory disk_factory;//控制飞碟的实例化
...
//2个飞碟队列,一个存储可用的飞碟,一个存储在游戏场景中、仍未被击落的飞碟
//但是,飞行动作不由飞碟自己控制,飞行动作类不挂载在飞碟上
//所以FirstController无法直接调用飞行动作
private Queue<GameObject> disk_queue = new Queue<GameObject>();
private List<GameObject> disk_notshot = new List<GameObject>();
FlyActionManager:
//FlyActionManager
//将FlyAction的接口转为FlyActionManager,使得FirstManager可以访问
public DiskFlyAction 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 = DiskFlyAction.GetSSAction(disk.GetComponent<DiskComponent>().direction, angle, power);
this.RunAction(disk, fly, this);
}
1.2使飞碟同时支持物理运动与运动学(变换)运动
物理运动:
游戏世界对象赋予现实世界物理属性(重量、形状等),并抽象为刚体(Rigid)模型(也包括滑轮、绳索等),使得游戏物体在力的作用下,仿真现实世界的运动及其之间的碰撞。简而言之,物体在力的作用下运动。
变换运动:
运用几何学的方法来研究物体的运动,通常不考虑力和质量等因素的影响,即游戏对象位置的直接改变,而不是在力的作用下改变。
在上一次的作业中,我实现的运动是物理运动:
//DiskFlyAction
public static DiskFlyAction GetSSAction(Vector3 direction, float angle, float power)
{
DiskFlyAction action = CreateInstance<DiskFlyAction>();
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()
{
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;
}
改进:增加一个布尔变量flag,如果为true则飞碟进行变换运动;否则进行物理运动。
//FirstController
bool flag = false;
action_manager.UFOFly(disk, angle, power, flag);
//FlyActionManager
public void UFOFly(GameObject disk, float angle, float power, bool flag)
{
fly = DiskFlyAction.GetSSAction(disk.GetComponent<DiskComponent>().direction, angle, power,flag);
this.RunAction(disk, fly, this);
}
//DiskFlyAction
Vector3 delta_vector = new Vector3(0, -3, 0);
DiskFlyAction action = CreateInstance<DiskFlyAction>();
if (flag)//变换运动,不受力的影响
{
action.start_vector += delta_vector;
}
else //物理运动,模拟重力影响下的抛体运动
{
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;
}
GitHub链接请见博客最底部~
2、打靶游戏(可选作业):
游戏内容要求:
靶对象为 5 环,按环计分;
箭对象,射中后要插在靶上
增强要求:射中后,箭对象产生颤抖效果,到下一次射击 或 1秒以后
游戏仅一轮,无限 trials;
增强要求:添加一个风向和强度标志,提高难度
游戏效果图:
游戏规则及玩法:
1.射箭打靶游戏,靶子共5环,从外到里对应1~5分,脱靶不扣分;
2.玩家用鼠标操控,用鼠标的位置控制发射的方向;鼠标左键发射。箭发射后1秒,会自动生成新的箭;
3.箭在飞行过程中会受到风力的影响,风向和风力大小会显示在屏幕上,玩家需要考虑风力,预判发射的方向。
下面介绍实现过程。
首先,制作箭和靶子。我都采用unity自带的简单3D游戏对象,比如cube和cylinder。
弓箭是由一个cylinder作为箭杆,由两个cube作为箭头:
再给箭杆添加棕色即可。
靶子的制作也很简单,在同一位置,将大小不同的cylinder叠在一起(注意厚度要调低),颜色设置成红白相间:
将箭和靶子加入预设。为了方便显示分数和风向,我又将两个Text添加到了预设。
本次打靶小游戏设计仍然采用了MVC模式和工厂模式。由一个弓箭工厂类来负责弓箭的实例化,并确保该工厂只有一个实例(单例模式)。和打飞碟游戏类似,弓箭工厂也维护两个list,正在游戏场景中被使用的弓箭以及空闲可被实例化的弓箭。这样管理,便于实现对资源的回收和利用。
· ArrowFactory 弓箭工厂
public class ArrowFactory : System.Object
{
private static ArrowFactory instance;
private GameObject ArrowPrefabs;
private List<GameObject> usingArrowList = new List<GameObject>();
private List<GameObject> freeArrowList = new List<GameObject>();
private Vector3 INITIAL_POS = new Vector3(0, 0, -19);
public static ArrowFactory getInstance()
{
if (instance == null) instance = new ArrowFactory();
return instance;
}
public void init(GameObject _arrowPrefabs)
{
ArrowPrefabs = _arrowPrefabs;
}
internal void detectArrowsReuse()
{
for(int i = 0; i < usingArrowList.Count; i++)
{
if(usingArrowList[i].transform.position.y <= -8)
{
usingArrowList[i].GetComponent<Rigidbody>().isKinematic = true;
usingArrowList[i].SetActive(false);
usingArrowList[i].transform.position = INITIAL_POS;
freeArrowList.Add(usingArrowList[i]);
usingArrowList.Remove(usingArrowList[i]);
i--;
SceneController.getInstance().changeWind();
}
}
}
internal GameObject getArrow()
{
if(freeArrowList.Count == 0)
{
GameObject newArrow = Camera.Instantiate(ArrowPrefabs);
usingArrowList.Add(newArrow);
return newArrow;
}
else
{
GameObject oldArrow = freeArrowList[0];
freeArrowList.RemoveAt(0);
oldArrow.SetActive(true);
usingArrowList.Add(oldArrow);
return oldArrow;
}
}
}
· ArrowController 弓箭管理类
本类主要实现的功能是,弓箭在风力的影响下位置变动。用一组向量数组保存风力的8个风向,可以根据风向直接得到对应的向量,然后乘以风力大小便可以很容易地得到一个风力。
public class ArrowController : MonoBehaviour {
public GameObject TargetPrefabs, ArrowPrefabs;
private GameObject holdingArrow, target;
private const int SPEED = 40;//飞行速度
private SceneController scene;
private Vector3[] winds = new Vector3[8]//八个风力方向
{
new Vector3(0, 1, 0), new Vector3(1,1,0), new Vector3(1,0,0), new Vector3(1,-1,0), new Vector3(0,-1,0), new Vector3(-1,-1,0), new Vector3(-1,0,0), new Vector3(-1,1,0)
};
private void Awake()
{
ArrowFactory.getInstance().init(ArrowPrefabs);
}
// Use this for initialization
void Start () {
scene = SceneController.getInstance();
scene.setArrowController(this);
target = Instantiate(TargetPrefabs);
}
// Update is called once per frame
void Update () {
ArrowFactory.getInstance().detectArrowsReuse();
}
public bool ifReadyToShoot()
{
return (holdingArrow != null);
}
internal void getArrow()
{
if (holdingArrow == null) holdingArrow = ArrowFactory.getInstance().getArrow();
}
internal void shootArrow(Vector3 mousePos)
{
holdingArrow.transform.LookAt(mousePos * 30);
holdingArrow.GetComponent<Rigidbody>().isKinematic = false;
addWind(); //风力
holdingArrow.GetComponent<Rigidbody>().AddForce(mousePos * 30, ForceMode.Impulse);
holdingArrow = null;
}
private void addWind()
{
int windDir = scene.getWindDirection();
int windStrength = scene.getWindStrength();
Vector3 windForce = winds[windDir]*50;
holdingArrow.GetComponent<Rigidbody>().AddForce(windForce, ForceMode.Force);
}
}
· GameStateController 状态管理类
本类主要实现两个功能,一个是创建大小随机、方向随机的风力并通知ArrowController施加风力;另一个是实现分数的控制,并负责输出相关内容(比如命中的分数、总分数)。
public class GameStateController : MonoBehaviour {
public int i = 0;
public GameObject canvasPrefabs, scoreTextPrefabs, tipsTextPrefabs, windTextPrefabs;
private int score = 0, windDir = 0, windStrength = 0;
private const float TIPS_SHOW_TIME = 0.5f;
private GameObject canvas, scoreText, tipsText, windText;
private SceneController scene;
private string[] windDirectionArray;
// Use this for initialization
void Start () {
scene = SceneController.getInstance();
scene.setGameController(this);
canvas = Instantiate(canvasPrefabs);
scoreText = Instantiate(scoreTextPrefabs, canvas.transform);
tipsText = Instantiate(tipsTextPrefabs, canvas.transform);
windText = Instantiate(windTextPrefabs, canvas.transform);
scoreText.GetComponent<Text>().text = "Score: " + score;
tipsText.SetActive(false);
windDirectionArray = new string[8] { "↑", "↗", "→", "↘", "↓", "↙", "←", "↖" };
changeWind();
}
public void changeWind()
{
windDir = UnityEngine.Random.Range(0,8);
windStrength = UnityEngine.Random.Range(0, 8);
windText.GetComponent<Text>().text = "Wind: " + windDirectionArray[windDir] + " x" + windStrength;
}
internal void addScore(int point)
{
i++;
if(i%2==0)
score += point;
scoreText.GetComponent<Text>().text = "Score: " + score;
changeWind();
}
// Update is called once per frame
void Update () {
}
internal int getWindDirec()
{
return windDir;
}
internal int getWindStrength()
{
return windStrength;
}
//提示命中环数
internal void showTips(int point)
{
tipsText.GetComponent<Text>().text = point + " Points!\n";
switch (point)
{
case 1:
tipsText.GetComponent<Text>().text += "Poor!";
break;
case 2:
tipsText.GetComponent<Text>().text += "Fair!";
break;
case 3:
tipsText.GetComponent<Text>().text += "Average!";
break;
case 4:
tipsText.GetComponent<Text>().text += "Good!";
break;
case 5:
tipsText.GetComponent<Text>().text += "Excellent!";
break;
}
tipsText.SetActive(true);
StartCoroutine(waitForTipsDisappear());
}
private IEnumerator waitForTipsDisappear()
{
yield return new WaitForSeconds(TIPS_SHOW_TIME);
tipsText.SetActive(false);
}
}
· SceneController 场记
主要给出需要实现的接口,使项目逻辑更加清晰。
namespace Scene
{
public interface IUserAction
{
void getArrow();
void shootArrow(Vector3 mousePos);
}
public interface ISceneController
{
void addScore(int point);
void showTips(int point);
void changeWind();
int getWindDirection();
int getWindStrength();
bool ifReadyToShoot();
}
public class SceneController : System.Object, IUserAction, ISceneController
{
private static SceneController instance;
private GameStateController gameStateController;
private ArrowController arrowController;
public static SceneController getInstance()
{
if (instance == null) instance = new SceneController();
return instance;
}
public void setGameController(GameStateController gsc)
{
gameStateController = gsc;
}
public void setArrowController(ArrowController _arrowController)
{
arrowController = _arrowController;
}
public void addScore(int point)
{
gameStateController.addScore(point);
}
public void changeWind()
{
gameStateController.changeWind();
}
public void getArrow()
{
arrowController.getArrow();
}
public int getWindDirection()
{
return gameStateController.getWindDirec();
}
public int getWindStrength()
{
return gameStateController.getWindStrength();
}
public bool ifReadyToShoot()
{
return arrowController.ifReadyToShoot();
}
public void shootArrow(Vector3 mousePos)
{
arrowController.shootArrow(mousePos);
}
public void showTips(int point)
{
gameStateController.showTips(point);
}
}
}
· ArrowCollider 碰撞检测
游戏中主要的碰撞事件是箭头碰撞靶子,且不同的靶环对应不同的分数增加。实现的思路是,制作预设的时候,给靶子都加一个"target"标签,并且根据各个靶环的名字末尾的编号来加响应的分数(我设置的名字为:C1,…,C5)。
借用OnTriggerEnter函数,该函数的作用条件是双方必须有一方设置Is Trigger为Enable且双方必须有一方设置有Rigidbody刚体组件。因此在设置属性的时候也需要小心控制,避免同一次碰撞触发两次OnTriggerEnter函数的现象。
public class ArrowCollider : MonoBehaviour {
private ISceneController scene;
void Start () {
scene = SceneController.getInstance() as ISceneController;
}
void Update () {
}
void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "target")
{
gameObject.transform.parent.gameObject.GetComponent<Rigidbody>().isKinematic = true;
gameObject.SetActive(false);
int points = other.gameObject.name[other.gameObject.name.Length - 1] - '0';
scene.addScore(points);
scene.showTips(points);
}
}
}
· UserInterface UI类
public class UserInterface : MonoBehaviour {
private IUserAction action;
private ISceneController scene;
bool gameStart = false;
int timeCount = 0;
// Use this for initialization
void Start () {
action = SceneController.getInstance() as IUserAction;
scene = SceneController.getInstance() as ISceneController;
}
void Update () {
if (!gameStart)
{
action.getArrow();
gameStart = true;
}
if (timeCount%75==0) action.getArrow();
if (scene.ifReadyToShoot())
{
//左键松开,射出弓箭
if (Input.GetMouseButtonUp(0))
{
Vector3 mousePos = Camera.main.ScreenPointToRay(Input.mousePosition).direction;
action.shootArrow(mousePos);
timeCount = 0;
}
}
timeCount++;
}
}
最后,放上GitHub传送门