Hit UFO(Food Edition)

游戏介绍

游戏规则

  1. 点击飞碟来摧毁它
  2. 飞碟飞出屏幕外时扣除生命值,生命值归零时游戏结束
  3. 点击特定种类(Ribs)飞碟时,生命值增加1
  4. 摧毁飞碟时会获得点数,随着点数增加,游戏难度增大

游戏演示

游戏视频链接

游戏代码

游戏代码链接

游戏实现

代码结构

飞碟工厂与数据管理器


动作管理

本次游戏的动作管理基本遵循之前的架构,其中由于使用了两套运动方案,故使用了适配器模式(adaptor)连接了CCActionManager与PhysisActionManager。

Adaptor

public class Adapter : MonoBehaviour
{
    // Start is called before the first frame update
    public CCActionManager actionManager;
    public PhysicActionManager physicActionManager;
    private FirstController sceneController; 
    void Start()
    {
        actionManager = gameObject.AddComponent<CCActionManager>();
        physicActionManager = gameObject.AddComponent<PhysicActionManager>();
        
        sceneController = (FirstController)SSDirector.getInstance ().currentSceneController;
        sceneController.actionManager = this;
    }

    public void playDisk(DiskData disk, bool mode){
        if(mode){
            actionManager.playDisk(disk);
        } else {
            physicActionManager.playDisk(disk);
        }
    }
    void Update()
    {
        
    }
}

CCAcionManager

public class CCActionManager : SSActionManager, ISSActionCallback, IActionManager{
    
    private FirstController sceneController;
    CCFlyAction action;

    protected  void Start() {
        sceneController = (FirstController)SSDirector.getInstance ().currentSceneController;
        base.Start();
    }

    // Update is called once per frame
    
    protected  void Update ()
    {
        base.Update();
    }
        
    #region ISSActionCallback implementation
    public void SSActionEvent (SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null)
    {
    }

    public void playDisk(DiskData disk){
        DataManager.MoveData m = sceneController.dataManager.GetMoveData(disk);

        action = CCFlyAction.GetSSAction(m.speed, m.angle, m.gravity); 
        RunAction(disk.gameObject, action, this);
    }
    #endregion
}

CCFlyAction

public class CCFlyAction : SSAction
{
    public float Power = 10f;
    public float Angle = 45f;
    public float Gravity = -10f;
    private Vector3 XSpeed; 
    private Vector3 YSpeed;
    private DataManager dataManager;
    private DiskFactory diskFactory;

    
    public static CCFlyAction GetSSAction(float Power, float Angle, float Gravity){
        CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction> ();
        action.Power = Power;
        action.Angle = Angle;
        action.Gravity = Gravity; 
        action.XSpeed = new Vector3(1,0,0) * Mathf.Cos(action.Angle / 180 * Mathf.PI) * action.Power;
        action.YSpeed = new Vector3(0,1,0) * Mathf.Sin(action.Angle / 180 * Mathf.PI) * action.Power;
        return action;
    }

    public override void Update ()
    { 
        transform.position += (XSpeed + YSpeed) * Time.deltaTime;
        YSpeed += Gravity * new Vector3(0,1,0) * Time.deltaTime;
        if (!dataManager.IsInScene(transform.position.x, transform.position.y)  && gameobject.activeSelf) {
            //waiting for destroy
            this.destory = true;  
            this.callback.SSActionEvent (this);
            dataManager.DeductLife();
            diskFactory.FreeDisk(gameobject);
        }
    }

    public override void Start () { 
        FirstController sceneController = (FirstController)SSDirector.getInstance ().currentSceneController;
        dataManager = sceneController.dataManager;
        diskFactory = sceneController.diskFactory;
    }
}

PhysisActionManager

public class PhysicActionManager : SSActionManager, ISSActionCallback, IActionManager
{
    // Start is called before the first frame update
    private PhysicFlyAction action;
    protected void Start()
    {
        base.Start();
    }

    // Update is called once per frame
    protected void Update()
    {
        base.Update();
    }

    #region ISSActionCallback implementation
    public void SSActionEvent (SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null)
    {
    }

    public void playDisk(DiskData disk){
        action = PhysicFlyAction.GetSSAction(disk.speed, disk.angle);
        RunAction(disk.gameObject, action, this);
    }
    #endregion
}

PhysisFlyAction

public class PhysicFlyAction : SSAction
{
    private Vector3 XSpeed;
    private Vector3 YSpeed;
    private Rigidbody rb;
    private DataManager dataManager;
    private DiskFactory diskFactory;
    public static PhysicFlyAction GetSSAction(float Speed, float Angle){
        PhysicFlyAction action = ScriptableObject.CreateInstance<PhysicFlyAction> ();
        action.XSpeed = Speed * Mathf.Cos(Angle / 180 * Mathf.PI) * new Vector3(1, 0, 0);
        action.YSpeed = Speed * Mathf.Sin(Angle / 180 * Mathf.PI) * new Vector3(0, 1, 0);
        return action;
    }

    public override void Update ()
    {
        if (!dataManager.IsInScene(transform.position.x, transform.position.y) && gameobject.activeSelf) {
            //waiting for destroy
            diskFactory.FreeDisk(gameobject);
            dataManager.DeductLife();
            this.destory = true;  
            this.callback.SSActionEvent (this);
        }
    }

    public override void Start () {
        FirstController sceneController = (FirstController)SSDirector.getInstance ().currentSceneController;
        dataManager = sceneController.dataManager;
        diskFactory = sceneController.diskFactory;
        gameobject.AddComponent<Rigidbody>();
        rb = gameobject.GetComponent<Rigidbody>();
        rb.velocity = XSpeed + YSpeed;
    }
}

对象管理

对象管理主要通过工厂模式与对象池实现,场景控制器可在DiskFactorry获取对象与释放对象,而DiskFactory则创建对象池回收释放的对象,提高对象的利用率。

DiskFactory

public class DiskFactory : MonoBehaviour
{
    public GameObject diskPrefab;
    private List<DiskData> used = new List<DiskData>();
    private List<DiskData> free = new List<DiskData>();
    // private FirstController sceneController;
    private DataManager dataManager;

    public GameObject GetDisk (int round) {
        int choice;
        int scope1 = 1, scope2 = 5, scope3 = 8, scope4 = 10;                                       
        string tag;
        diskPrefab = null;
        choice = Random.Range(0, scope4);

        if (choice <= scope1) {
            tag = "Apple";
        }
        else if (choice <= scope2 && choice > scope1) {
            tag = "Orange";
        }
        else if (choice <= scope3 && choice > scope2){
            tag = "Tomato";
        } else {
            tag = "Ribs";
        }

        for(int i=0;i<free.Count;i++) {
            if(free[i].gameObject.CompareTag(tag)) {
                diskPrefab = free[i].gameObject;
                free[i].gameObject.SetActive(true);
                free.Remove(free[i]);
                break;
            }
        }
        if (diskPrefab == null) {
            GameObject prefab =  Resources.Load("Prefabs/" + tag, typeof(GameObject)) as GameObject;
            diskPrefab = Instantiate(prefab,new Vector3(0, 0, 0), prefab.transform.rotation);
        }
        float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
        used.Add(diskPrefab.GetComponent<DiskData>());
        
        // 随机生成位置
        float Xedge = 5;
        float Yedge = 3.5f;
        
        choice = Random.Range(1, 4);
        if (choice == 1) {
            diskPrefab.transform.position = new Vector3(-Xedge, Random.Range(0, Yedge / 2f), -1);
            diskPrefab.GetComponent<DiskData>().angle = Random.Range(-30, 60);
        } else if (choice == 2) {
            diskPrefab.transform.position = new Vector3(Random.Range(-Xedge / 3f, Xedge / 3f), Yedge, -1);
            diskPrefab.GetComponent<DiskData>().angle = Random.Range(-135, -45);
        } else {
            diskPrefab.transform.position = new Vector3(Xedge, Random.Range(0, Yedge / 2f), -1);
            diskPrefab.GetComponent<DiskData>().angle = Random.Range(-225, -135);
        }
        return diskPrefab;
    }

    private void IniDisk (GameObject disk) {
        if (disk.GetComponent<Rigidbody>() != null) {
            Destroy(disk.GetComponent<Rigidbody>());
        }
    }

    public void FreeDisk (GameObject disk) {
        for (int i = 0; i < used.Count; i++) {
            if (used[i].gameObject == disk) {
                IniDisk(used[i].gameObject);
                used[i].gameObject.SetActive(false);
        
                free.Add(used[i]);
                used.Remove(used[i]);
                break;
            }
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        FirstController sceneController = (FirstController)SSDirector.getInstance ().currentSceneController;
        sceneController.diskFactory = this;
        dataManager = sceneController.dataManager;
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

DiskData

public class DiskData : MonoBehaviour
{
    public int score;
    public float speed;
    public float angle;  
    public string foodName;                               
}

用户交互

用户交互部分与往期作业相比变化不大,是使场景控制器实现IUserAction完成的。

UserGUI

public class UserGUI : MonoBehaviour
{
    public FirstController userAction;
    private String ButtonName1 = "Start";
    private String ButtonName2 = "Physis";
    public GUIStyle customButton;
    public GUIStyle customLabel;
    public GameObject catchFood, gameOver;

    private float w;
    private float h;
    private int score;
    private int life;
    private bool IsGameOver;
    // Start is called before the first frame update
    void Start()
    {
        userAction =  (FirstController)SSDirector.getInstance ().currentSceneController;
        userAction.userGUI = this;
        w = Screen.width;
        h = Screen.height;
        gameOver.SetActive(false);
        catchFood.SetActive(true);        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    void OnGUI()
    {
        if (IsGameOver) {
            userAction.GameOver();
            gameOver.SetActive(true);
            catchFood.SetActive(false);
        }

        if (GUI.Button(new Rect(h * 0.02f, h * 0.08f, w * 0.15f, h * 0.065f),ButtonName1,customButton)) {
            if (ButtonName1 == "Start") {
                userAction.GameStart();
                ButtonName1 = "Restart";
            } else {
                userAction.GameRestart();
                ButtonName1 = "Start";
                gameOver.SetActive(false);
                catchFood.SetActive(true);
            }
        }

        if (GUI.Button(new Rect(h * 0.02f, h * 0.18f, w * 0.15f, h * 0.065f),ButtonName2,customButton)) {
            if (ButtonName2 == "Physis") {
                ButtonName2 = "Kinematics";
                userAction.GameModeSet(false);
            } else {
                ButtonName2 = "Physis";
                userAction.GameModeSet(true);
            }
            
        }        
        GUI.Label(new Rect(h * 0.02f, 20, 100, 50),"Score:" + score, customLabel);
        GUI.Label(new Rect(w * 0.15f, 20, 100, 50), "Life:" + life, customLabel);
    }

    public void SetScore(int score){
        this.score = score;
    }

    public void SetLife(int life){
        this.life = life;
    }

    public void SetIsGameOver(bool IsGameOver){
        this.IsGameOver = IsGameOver;
    }
}

数据管理

本次作业中要求实现ScoreManager,但是由于本游戏中同时包含生命值,游戏round等数值,最终决定通过创建DataManager进行统一地数据管理。

DataManager

public class DataManager : MonoBehaviour
{
    private FirstController sceneController;
    public GameObject background;
    private int score = 0; 
    private int live;
    public float interval = 1f;
    public int round = 1;
    public float gravity = -10f; 
    public bool mode = true;
    public struct EdgeData{
        public float Xedge;
        public float Yedge;
    }

    public struct MoveData{
        public float gravity;
        public float speed;
        public float angle;
    }
    // Start is called before the first frame update
    void Start()
    {
        sceneController = (FirstController)SSDirector.getInstance ().currentSceneController;
        sceneController.dataManager = this;
        Physics.gravity = new Vector3(0,gravity,0);
    }

    // Update is called once per frame
    void Update()
    {
        if (score >= (int)Mathf.Pow(round, 1.5f) * 200) round++;
    }

    public void Init(){
        score = 0;
        live = 5;
        round = 1;
        sceneController.userGUI.SetIsGameOver(false);
        sceneController.userGUI.SetScore(score);
        sceneController.userGUI.SetLife(live);
    }

    public void UpdateScore(GameObject disk) {
        DiskData diskData = disk.GetComponent<DiskData>();
        score += diskData.score * (int)Math.Sqrt(round);
        interval = 1f - round * 0.1f;
        sceneController.userGUI.SetScore(score);
    }

    public void DeductLife() {
        if (live > 0) live--;
        sceneController.userGUI.SetLife(live);
        if (live == 0) sceneController.userGUI.SetIsGameOver(true);
    }

    public void AddLife() {
        live++;
        sceneController.userGUI.SetLife(live);
    }

    public MoveData GetMoveData(DiskData disk){
        MoveData m;
        m.gravity = gravity;
        m.speed = disk.speed;
        m.angle = disk.angle;
        return m;
    }

    public EdgeData GetEdgeData(){
        EdgeData e;
        e.Xedge = background.transform.localScale.x / 2f;
        e.Yedge = background.transform.localScale.y / 2f;
        return e;
    }

    public bool IsInScene(float x, float y) {
        EdgeData e;
        e.Xedge = background.transform.localScale.x / 2f;
        e.Yedge = background.transform.localScale.y / 2f;
        if (x >= -e.Xedge && x <= e.Xedge && y >= -e.Yedge) return true;
        return false;
    }
}

场景控制

场景控制器通过调用ActionManager、DataManager、DiskFactory提供的方法,完成整个场景的调度。

FirstController

public class FirstController : MonoBehaviour, ISceneController, IUserAction {

    public Adapter actionManager { get; set;}
    public DiskFactory diskFactory{ get; set; }
    public DataManager dataManager{ get; set; }
    public UserGUI userGUI{ get; set; }
    public List<GameObject> flyingDisks = new List<GameObject>(); 
    private float timer = 0f;
    public bool IsRunning = false;

    // the first scripts
    void Awake () {
        SSDirector director = SSDirector.getInstance ();
        director.setFPS (60);
        director.currentSceneController = this;
        director.currentSceneController.LoadResources ();
        Debug.Log ("awake FirstController!");
    }
     
    // loading resources for first scence
    public void LoadResources () {
        ;
    }

    public void Pause ()
    {
        throw new System.NotImplementedException ();
    }

    public void Resume ()
    {
        throw new System.NotImplementedException ();
    }

    #region IUserAction implementation
    public void GameOver ()
    {
        SSDirector.getInstance ().NextScene ();
        IsRunning = false;
    }

    public void GameStart() {
        IsRunning = true;
        dataManager.Init();
    }

    public void GameRestart() {
        IsRunning = true;
        dataManager.Init();
    }

    public void GameModeSet(bool mode){
        dataManager.mode = mode;
    }
    #endregion


    // Use this for initialization
    void Start () {
        //give advice first
    }
    
    // Update is called once per frame
    void Update () {
        if (IsRunning) {
            if(Input.GetMouseButton(0)){
                Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
                RaycastHit hit;
                if(Physics.Raycast(ray,out hit)){
                    //划出射线,只有在scene视图中才能看到
                    Debug.DrawLine(ray.origin,hit.point);
                    GameObject gameObject = hit.collider.gameObject;
                    if(gameObject.GetComponent<DiskData>().foodName == "Ribs") dataManager.AddLife();
                    dataManager.UpdateScore(gameObject);
                    diskFactory.FreeDisk(gameObject);
                }
            }

            timer += Time.deltaTime;
            if (timer >= dataManager.interval) {
                timer = 0f;
                GameObject disk = diskFactory.GetDisk(dataManager.round);
                actionManager.playDisk(disk.GetComponent<DiskData>(), dataManager.mode);
            }
            
        }
        
    }
}