一、作业要求
1、编写一个简单的鼠标打飞碟(Hit UFO)游戏。
游戏内容要求:
1、游戏有 n 个 round,每个 round 都包括10 次 trial;
2、每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
3、每个 trial 的飞碟有随机性,总体难度随 round 上升;
4、鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
游戏的要求:
1、使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类;
2、近可能使用前面 MVC 结构实现人机交互与游戏模型分离;
3、按 adapter模式 设计图修改飞碟游戏,使它同时支持物理运动与运动学(变换)运动。
三、项目演示
视频演示
四、动作管理器
并且在本次实现中,有用到动作管理器:
适配器
适配器将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器的优点:
将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。适配器还增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
五、实现过程和方法(算法)
运动学和物理学运动方式,主要的区别在于,物理学执行动作的时候不需要我们使用transform.Translate()。
并且通过前几次作业的结合,可以简单画出本次作业程序的结构:
Director
Director类和上次的编写一模一样,也是利用单例模式和懒汉模式。
SceneController
SceneController也是一个接口,但这一次的游戏并不需要预先加载角色(上次要加载牧师、恶魔、船之类的对象,但这次没有),所以loadResources在FirstController的实现为空。
public interface ISceneControl
{
void loadResources();
}
UserAction
UserAction是门面模式,与用户交互相关的,与玩家动作有关。
public enum GameState { ROUND_START, ROUND_FINISH, RUNNING, PAUSE, START, FUNISH }
public interface IUserAction
{
GameState getGameState();//获取游戏状态
void setGameState(GameState gameState);//设置游戏状态
int getScore();//获取当前积分
void hit(Vector3 pos);//
bool getActionMode();//获取模式状态(运动学还是动力学)
void setActionMode(bool mode);//设置模式
}
FristSceneControl
FristSceneControl就要实现上面(SceneController、UserAction)两个类的函数。
1、 这次作业需要实现让运动学和动力学两种模式的共存,因此就需要利用适配器。对于FirstSceneActionManager,有两个类,一个是CCActionManager,负责运动学的行为;另一个是ModifiedActionManager,负责物理学的行为。
· ScoreRecorder类是专门负责记录成绩的
public ActionManagerAdapter actionManager { set; get; }//适配器
public ScoreRecorder scoreRecorder { set; get; } //成绩记录器
public Queue<GameObject> diskQueue = new Queue<GameObject>();//飞碟对象队列
private int diskNumber = 0;//飞碟的数量,主要用于设定每一轮抛出的飞碟数量
private int currentRound = -1;//当前进行到第几局
private float time = 0;//时间控制,用来做抛飞碟的时候用到
private GameState gameState = GameState.START;//游戏状态
UserGUI userGUI;//挂载GUI
private bool isPhysical = false;//用于判断是否使用物理引擎
2、对于项目的初始化(Awake):
void Awake()
{
Director director = Director.getInstance();//获取导演
director.current = this;
diskNumber = 10;//初始化每轮抛出的飞碟数
this.gameObject.AddComponent<ScoreRecorder>();//挂载ScoreRecorder
this.gameObject.AddComponent<DiskFactory>();//挂载DiskFactory
scoreRecorder = Singleton<ScoreRecorder>.Instance;
userGUI = gameObject.AddComponent <UserGUI>() as UserGUI;//挂载UserGUI
director.current.loadResources();
}
· scoreRecorder = Singleton<ScoreRecorder>.Instance;使得我们获取ScoreRecorder的单例对象。Singleton类的写法就是老师给出的模板类。
3、SceneController中的loadResources实现,本次为空。
public void loadResources()
{
}
4、UserAction的getGameState(),目的是将变量gameState进行return
5、UserAction的setGameState(GameState gameStateIn),设置变量gameStat,将gameStat设置为传入的参数即可
6、UserAction的getScore(),返回变量scoreRecorder.score
7、UserAction的hit(Vector3 pos)主要利用光标拾取多个物体的程序:
public void hit(Vector3 pos)
{
RaycastHit[] hits = Physics.RaycastAll(Camera.main.ScreenPointToRay(pos));
for (int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
if (hit.collider.gameObject.GetComponent<DiskData>() != null)
{
scoreRecorder.record(hit.collider.gameObject);
hit.collider.gameObject.transform.position = new Vector3(0, -5, 0);
}
}
}
8、UserAction的getActionMode(),返回变量isPhysical即可。
9、UserAction的setActionMode(bool mode),设置变量isPhysical为传入参数。
ActionManagerAdapter
因为FristSceneContoller要调用ActionManager的函数,并且这次由于有两种模式的存在,所以FristSceneContoller会通过ActionManagerAdapter(适配器)来调用相关模式下的函数。
适配器本质上就是一个接口,而继承适配器的类就要实现这些接口,也通过适配器进行了统一。
然后就是运动学CCActionManager和物理学ModifiedActionManager各自对这个函数进行实现。
CCActionManager与ModifiedActionManager
这两个类在这次的作业中其实代码差不多,主要区别是Update中一个要使用刚体一个不用。
对于CCActionManager:
protected void Start()
{
sceneControl = (FirstSceneControl)Director.getInstance().current;
sceneControl.actionManager = this;
flys.Add(CCFlyAction.getCCFlyAction());
base.flag = true;//这个flag如果是true表示使用运动学,false表示使用物理学
}
private new void Update()
{
if (sceneControl.getGameState() == GameState.RUNNING)
base.Update();
}
而ModifiedActionManager:
protected void Start()
{
sceneControl = (FirstSceneControl)Director.getInstance().current;
sceneControl.actionManager = this;
flys.Add(CCFlyAction.getCCFlyAction());
base.flag = false;//使用物理学
}
private new void Update()
{
if (sceneControl.getGameState() == GameState.RUNNING)
{
base.Update();
base.startRigidbodyAction();//启动刚体
}
else
{
base.stopRigidbodyAction();//停止刚体
}
}
SSActionManager
ActionManager跟上次作业的基本相似,多了要判断哪一种运动方式用不同的update
protected void Update()
{
//......
foreach (KeyValuePair<int, SSAction> i in actions)
{
SSAction value = i.Value;
if (value.destroy)
{
waitingDelete.Add(value.GetInstanceID());
}
else if (value.enable && flag)//flag就是对应上面说使用哪种运动方式的变量
{
value.Update();
}
else if(value.enable && !flag)
{
value.FixedUpdate();
}
}
}
SSAction
SSAction也实际上是一个接口,在本次作业的程序中,类CCFlyAction就负责实现这个最底部被继承的动作类。
CCFlyAction
CCFlyAction是负责实现飞碟飞行动作的类,它负责设置了大部分的变量参数。
float acceleration;//加速度
float horizontalSpeed;//水平速度
Vector3 direction;
float time;
bool flag=false;//记录游戏是否经过暂停状态
Vector3 temp;//记录游戏暂停时刚体游戏对象的速度矢量
Rigidbody rigidbody; //物理运动,添加刚体
//变量的初始化
public override void Start()
{
enable = true;
acceleration = 9.8f;
time = 0;
horizontalSpeed = gameObject.GetComponent<DiskData>().getSpeed();
direction = gameObject.GetComponent<DiskData>().getDirection();
//执行行为的对象如果有刚体性质,则需要设置刚体的速度属性
rigidbody = gameObject.GetComponent<Rigidbody>();
if (rigidbody)
{
rigidbody.velocity = horizontalSpeed * direction;
temp = rigidbody.velocity;
}
}
DiskFactory
DiskFactory给我感觉有点像线程池,预先创建了一些飞碟,当需要用的时候才会取出。
对于一个飞碟,有以下的一些参数:
private Vector3 size;//大小
private Color color;//颜色
private float speed;//运动速度
private Vector3 direction;//位置
而工厂应该要有一个空闲队列和使用队列,用来记录哪些飞碟目前还没被使用,哪些已经被Active:
public List<DiskData> used = new List<DiskData>();
public List<DiskData> free = new List<DiskData>();
工厂的Awake()函数是加载飞碟对象的预建,并且设置它非Active:
private void Awake()
{
diskPrefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Disk"), Vector3.zero, Quaternion.identity);
diskPrefab.SetActive(false);
}
此时的飞碟的参数都并没有被设置,也就是大小颜色那些都没有设定。
之后是从工厂种取出飞碟的getDisk()函数,传入的参数是当前第几轮(用以判断可以出什么颜色的飞碟)、是否使用刚体。getDisk()的工作主要有:
1、判断空闲队列还有没有飞碟,有的话直接取出空闲队列中的一个飞碟,并且把它从空闲队列移除。
2、如果空闲队列没有飞碟,则重新创建一个飞碟对象直接使用。
3、判断是否利用刚体,如果是的话,给刚刚所取出的飞碟套上刚体属性。
4、根据当前所进行到的回合数(约往后给出的飞碟会变小并且速度更快),设定飞碟的大小、颜色、速度、位置。
5、return这个飞碟对象
当一个飞碟使用完毕进行回收时,可以利用freeDisk(GameObject disk)函数。
1、要把它设置成非Active
2、把这个飞碟放到空闲队列,并且从使用队列中移除
ScoreRecorder
ScoreRecorder就是拿来记录不同颜色的飞碟对应的分数,以及当玩家击中飞碟时计算总得分。
因此它的初始化是设置总分为0、设置各种颜色飞碟对应多少分:
void Start() /* 初始化各种颜色的分数值 */
{
score = 0;
scoreTable.Add(Color.white, 1);
scoreTable.Add(Color.gray, 2);
scoreTable.Add(Color.black, 4);
}
当击中某个飞碟时,就可以调用record(GameObject disk)函数,把飞碟对象传入,让这个函数去计算得分:
public void record(GameObject disk)
{
score += scoreTable[disk.GetComponent<DiskData>().getColor()];
}
具体代码可以查看gitee链接
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。