作业六

本文详细介绍了使用Unity游戏引擎通过物理运动和运动学变换设计飞碟射击游戏的过程,以及如何设计一个复杂的打靶游戏,包括箭矢物理碰撞、颤抖效果和风向影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

改进飞碟游戏

要求

改进飞碟(Hit UFO)游戏:

  • 按 adapter模式设计图修改飞碟游戏
  • 使它同时支持物理运动与运动学(变换)运动

设计与实现

这次我们需要按照Adapter模式重写飞碟的控制逻辑,可以同时支持物理运动和运动学变换运动。
我们新定义一个接口IActionManager,使得我们可以自定义PlayDisk方法的逻辑实现:

public interface IActionManager
{
    void PlayDisk(Disk Disk);
    bool IsAllFinished();
}

我们在原来运动的基础上,添加了一个CCPhysisActionManager类,用以管理物理运动,这也是本次要实现的支持物理运动部分的代码:

public class CCPhysisActionManager : SSActionManager, SSActionCallback, IActionManager
{
    int count = 0;
    public SSActionEventType Complete = SSActionEventType.Completed;
    UserAction UserActionController;

    public void PlayDisk(Disk Disk)
    {
        Debug.Log("CCPhysisActionManager");
        count ++;
        Complete = SSActionEventType.Started;
        CCPhysisAction action = CCPhysisAction.getAction(Disk.speed);
        addAction(Disk.gameObject, action, this);
    }

    public void SSActionCallback(SSAction source, bool isHit)
    {
        count --;
        Complete = SSActionEventType.Completed;
        UserActionController = SSDirector.getInstance().currentScenceController as UserAction;
        if (!isHit)
        {
            UserActionController.ReduceHealth();
        }
        source.gameObject.SetActive(false);
    }

    public bool IsAllFinished()
    {
        Debug.Log("isALLFInished");
        if (count == 0)
            return true;
        else return false;
    }

}

底层的物理运动类CCPhysisAction的实现如下所示:

public class CCPhysisAction : SSAction
{
    public float speedx;

    private CCPhysisAction() {}

    public static CCPhysisAction getAction(float speedx)
    {
        CCPhysisAction action = CreateInstance<CCPhysisAction>();
        action.speedx = speedx;
        return action;
    }

    // Use this for initialization
    public override void Start()
    {
        if (!this.gameObject.GetComponent<Rigidbody>())
        {
            this.gameObject.AddComponent<Rigidbody>();
        }
        this.gameObject.GetComponent<Rigidbody>().AddForce(Vector3.up * 9.8f * 0.6f, ForceMode.Acceleration);
        this.gameObject.GetComponent<Rigidbody>().AddForce(new Vector3(speedx, 0, 0), ForceMode.VelocityChange);
    }

    // Update is called once per frame
    override public void Update()
    {
        if (transform.position.z == -1)
        {
            Debug.Log("Hit");
            destroy = true;
            CallBack.SSActionCallback(this, true);
        }
        else if (transform.position.y <= -45)
        {
            Debug.Log("Missing");
            Destroy(this.gameObject.GetComponent<Rigidbody>());
            destroy = true;
            CallBack.SSActionCallback(this, false);
        }
    }
}

最后我们只需要重新修改场记FirstSceneController即可:

public class FirstSceneController : MonoBehaviour, ISceneController, UserAction
{
    int score = 0;
    int round = 1;
    int tral = 0;
    int health = 5;
    bool start = false;
    bool gameOver = false;
    public bool PhysicManager = false;
    bool ManagerofNow = false;
    IActionManager Manager;
    DiskFactory DF;

    void Awake()
    {
        SSDirector director = SSDirector.getInstance();
        director.currentScenceController = this;
        DF = DiskFactory.DF;
        //Manager = GetComponent<CCActionManager>();
        ManagerofNow = PhysicManager;
        if (PhysicManager)
        {
            Manager = this.gameObject.AddComponent<CCPhysisActionManager>() as IActionManager;
        }
        else
        {
            Manager = this.gameObject.AddComponent<CCActionManager>() as IActionManager;
        }
    }

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    int count = 0;
    void Update()
    {
        if (health <= 0)
            gameOver = true;
        if (gameOver)
            return;
        if (start == true)
        {
            count ++;
            if (count >= 80)
            {
                count = 0;

                if (DF == null)
                {
                    Debug.LogWarning("DF is NUll!");
                    return;
                }
                tral ++;
                Disk d = DF.GetDisk(round);
                if (Manager == null)
                {
                    Debug.LogWarning("Manager is NULL!");
                    return;
                }
                Manager.PlayDisk(d);
                //Manager.MoveDisk(d);
                if (tral == 10)
                {
                    round ++;
                    tral = 0;
                }
            }
        }
    }

    public void LoadResources()
    {
 
    }

    public void Hit(Vector3 pos)
    {
        Ray ray = Camera.main.ScreenPointToRay(pos);
        RaycastHit[] hits;
        hits = Physics.RaycastAll(ray);
        for (int i = 0; i < hits.Length; i ++)
        {
            RaycastHit hit = hits[i];

            if (hit.collider.gameObject.GetComponent<Disk>() != null)
            {
                Color c = hit.collider.gameObject.GetComponent<Renderer>().material.color;
                if (c == Color.yellow)
                    score += 1;
                if (c == Color.red)
                    score += 2;
                if (c == Color.black)
                    score += 3;
                GameObject explosion = Instantiate(Resources.Load<GameObject>("Prefabs/ParticleSystem"), hit.collider.gameObject.transform.position, Quaternion.identity);
                explosion.GetComponent<ParticleSystem>().Play();
                Object.Destroy(explosion, 0.1f);
                hit.collider.gameObject.transform.position = new Vector3(0, -400, -1);
            }
        }
    }

    public int GetScore()
    {
        return score;
    }

    public int GetRound()
    {
        return round;
    }

    public int GetHealth()
    {
        return health;
    }

    public void ReduceHealth()
    {
        health -= 1;
        if (health < 0)
            health = 0;
    }

    public void GameOver()
    {
        gameOver = true;
    }

    public bool RoundStop()
    {
        if (round > 3 || health <= 0)
        {
            start = false;
            return Manager.IsAllFinished();
        }
        else
            return false;
    }

    public void Restart()
    {
        score = 0;
        round = 1;
        health = 5;
        start = true;
        gameOver = false;
    }
}

在主界面中,要切换物理运动,我们只需要勾选我们定义的PhysicManager变量为真即可:
在这里插入图片描述
在控制台的信息中,我们可以看到我们正在使用物理运动:
在这里插入图片描述

打靶游戏

要求

  • 靶对象为 5 环,按环计分
  • 箭对象,射中后要插在靶上
  • 射中后,箭对象产生颤抖效果,到下一次射击 或 1秒以后
  • 游戏仅一轮,无限 trials
  • 添加一个风向和强度标志,提高难度

设计与实现

本次打靶游戏主要的难点在于设计物理碰撞,当箭触碰到对应的圆环时,要相应的加上对应的分数,为了实现这一目标,我们首先创建靶的预制:
在这里插入图片描述
我们设计了五个圆柱体为靶的子对象,为了标明它们各自的分数,我们使用了Ring Data类来进行表示,这样在代码中我们就可以获取它们各自的分数了。然后为了接收和箭的物理碰撞,我们给每个圆柱体加上了Mesh Collider的属性,并添加上了碰撞检测的代码:

public class CollisionDetection : MonoBehaviour
{
    public FirstSceneController sceneController;         
    public ScoreRecorder recorder;                        

    void Start()
    {
        sceneController = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;
        recorder = Singleton<ScoreRecorder>.Instance;
    }

    void OnTriggerEnter(Collider arrowHead)
    {
        Debug.Log("TriggerEnter");
        Transform arrow = arrowHead.gameObject.transform.parent;
        if (arrow == null)
        {
            Debug.Log("arrow is null");
            return;
        }
        if (arrow.tag == "arrow")
        {
            arrow.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
            arrow.GetComponent<Rigidbody>().isKinematic = true;
            recorder.Record(this.gameObject);
            arrowHead.gameObject.gameObject.SetActive(false);
            arrow.tag = "hit";
        }
    }
}

对箭来说,我们同样添加了一个Capsule Collider的属性,并添加了刚体属性:
在这里插入图片描述
然后就是主程序了,这部分还是利用了工厂模式生产箭,运动部分添加了风向的影响:

public class ArrowFlyAction : SSAction
{
    public Vector3 force;                
    public Vector3 wind;

    private ArrowFlyAction() { }

    public static ArrowFlyAction GetSSAction(Vector3 wind)
    {
        ArrowFlyAction action = CreateInstance<ArrowFlyAction>();
        action.force = new Vector3(0, 0, 20);
        action.wind = wind;
        return action;
    }

    public override void Update() { }

    public override void FixedUpdate()
    {
        this.gameobject.GetComponent<Rigidbody>().AddForce(wind, ForceMode.Force);

        if (this.transform.position.z > 35 || this.gameobject.tag == "hit")
        {
            this.destroy = true;
            this.callback.SSActionEvent(this, this.gameobject);
        }
    }
    public override void Start()
    {
        gameobject.transform.parent = null;
        gameobject.GetComponent<Rigidbody>().velocity = Vector3.zero;
        gameobject.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse);
    }
}

我们还要求在打中靶之后,箭有一个颤抖效果,这个同样使用运动类来实现:

public class ArrowTremble : SSAction
{
    float radian = 0;
    float perRadian = 3f;
    float radius = 0.01f;
    Vector3 position;
    public float leftTime = 0.8f;

    private ArrowTremble() { }

    public override void Start()
    {
        position = transform.position;
    }

    public static ArrowTremble GetSSAction()
    {
        ArrowTremble action = CreateInstance<ArrowTremble>();
        return action;
    }

    public override void Update()
    {
        leftTime -= Time.deltaTime;
        if (leftTime <= 0)
        {
            transform.position = position;
            this.destroy = true;
            this.callback.SSActionEvent(this);
        }

        radian += perRadian;
        float dy = Mathf.Cos(radian) * radius;
        transform.position = position + new Vector3(0, dy, 0);
    }

    public override void FixedUpdate() {}

}

运动管理类的实现可参照上几个游戏的实现,这里就不再列出了。
我们还是使用工厂模式生产箭支:

public class ArrowFactory : MonoBehaviour
{

    public GameObject arrow = null;                             
    private List<GameObject> used = new List<GameObject>();     
    private Queue<GameObject> free = new Queue<GameObject>();   
    public FirstSceneController sceneControler;                 

    public GameObject GetArrow()
    {
        if (free.Count == 0)
        {
            arrow = Instantiate(Resources.Load<GameObject>("Prefabs/Arrow"));
        }
        else
        {
            arrow = free.Dequeue();
            if (arrow.tag == "hit")
            {
                arrow.GetComponent<Rigidbody>().isKinematic = false;
                arrow.transform.GetChild(0).gameObject.SetActive(true);
                arrow.tag = "arrow";
            }
            arrow.gameObject.SetActive(true);
        }

        sceneControler = (FirstSceneController)SSDirector.GetInstance().CurrentScenceController;
        Transform temp = sceneControler.bow.transform.GetChild(1);
        arrow.transform.position = temp.transform.position;
        arrow.transform.parent = sceneControler.bow.transform;
        used.Add(arrow);
        return arrow;
    }

    public void FreeArrow(GameObject arrow)
    {
        for (int i = 0; i < used.Count; ++ i)
        {
            if (arrow.GetInstanceID() == used[i].gameObject.GetInstanceID())
            {
                used[i].gameObject.SetActive(false);
                free.Enqueue(used[i]);
                used.Remove(used[i]);
                break;
            }
        }
    }
}

并使用单例模式实现场景单实例:

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    protected static T instance;
    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = (T)FindObjectOfType(typeof(T));
                if (instance == null)
                {
                    Debug.LogError("An instance of " + typeof(T)
                        + " is needed in the scene, but there is none.");
                }
            }
            return instance;
        }
    }
}

本次打靶游戏还有一个地方需要注意,那就是子摄像机的实现。如果我们只使用一个摄像机,那么会因为距离太远,看不清楚自己击中了几环,会影响游戏性。所以我们在离靶子比较近的距离上添加了一个子摄像机,并暂时设置成隐藏的效果,当箭距离靶子一定距离之后我们就进行展示,挂载代码如下:

public class ChildCamera : MonoBehaviour
{
    public bool isShow = false;
    public float leftTime;

    void Update()
    {
        if (isShow)
        {
            leftTime -= Time.deltaTime;
            if (leftTime <= 0)
            {
                this.gameObject.SetActive(false);
                isShow = false;
            }
        }
    }

    public void StartShow()
    {
        this.gameObject.SetActive(true);
        isShow = true;
        leftTime = 2f;
    }
}

为了不让子摄像机全屏显示,我们还设置了其显示的区域:
在这里插入图片描述
设置了子摄像机之后,我们也需要设置主摄像机,让主摄像机跟随弓的移动而移动:

public class CameraFlow : MonoBehaviour
{
    public GameObject bow;               
    public float smothing = 5f;
    Vector3 offset;

    void Start()
    {
        offset = transform.position - bow.transform.position;
    }

    void FixedUpdate()
    {
        Vector3 target = bow.transform.position + offset;
        transform.position = Vector3.Lerp(transform.position, target, smothing * Time.deltaTime);
    }
}

在整个游戏的过程中,我们新添加一个记分员的类来帮助我们统计游戏分数:

public class ScoreRecorder : MonoBehaviour
{
    public int score;
    public int targetScore;            
    public int arrowNumber;

    void Start()
    {
        score = 0;
        targetScore = 15;
        arrowNumber = 10;
    }

    public void Record(GameObject gameObject)
    {
        int temp = gameObject.GetComponent<RingData>().score;
        score = temp + score;
    }
}

在UI界面,为了更形象的展示我们剩余的箭支数量,我们可以使用字符’I’来进行展示,Gui代码如下:

public class UserGUI : MonoBehaviour
{
    private IUserAction action;
    GUIStyle score_style = new GUIStyle();
    GUIStyle text_style = new GUIStyle();
    GUIStyle bold_style = new GUIStyle();
    GUIStyle over_style = new GUIStyle();
    private bool game_start = false;

    // Use this for initialization
    void Start()
    {
        action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
        text_style.normal.textColor = new Color(0, 0, 0, 1);
        text_style.fontSize = 16;
        score_style.normal.textColor = new Color(1, 0, 0, 1);
        score_style.fontSize = 16;
        bold_style.normal.textColor = new Color(1, 0, 0);
        bold_style.fontSize = 16;
        over_style.normal.textColor = new Color(1, 1, 1);
        over_style.fontSize = 25;
    }

    void Update()
    {
        if (game_start && !action.GetGameover())
        {
            if (Input.GetButtonDown("Fire1"))
            {
                action.Shoot();
            }
            float translationY = Input.GetAxis("Vertical");
            float translationX = Input.GetAxis("Horizontal");
            action.MoveBow(translationX, translationY);
        }
    }

    private void OnGUI()
    {
        if (game_start)
        {
            if (!action.GetGameover())
            {
                GUI.Label(new Rect(10, 5, 200, 50), "Score:", text_style);
                GUI.Label(new Rect(65, 5, 200, 50), action.GetScore().ToString(), score_style);

                GUI.Label(new Rect(Screen.width / 2 - 40, 8, 200, 50), "Target score:", text_style);
                GUI.Label(new Rect(Screen.width / 2 + 60, 8, 200, 50), action.GetTargetScore().ToString(), score_style);

                GUI.Label(new Rect(Screen.width - 170, 5, 50, 50), "Arrows:", text_style);
                for (int i = 0; i < action.GetResidueNum(); ++ i)
                {
                    GUI.Label(new Rect(Screen.width - 110 + 10 * i, 5, 50, 50), "I ", bold_style);
                }
                GUI.Label(new Rect(Screen.width - 170, 30, 200, 50), "winds: ", text_style);
                GUI.Label(new Rect(Screen.width - 110, 30, 200, 50), action.GetWind(), text_style);
            }

            if (action.GetGameover())
            {
                GUI.Label(new Rect(Screen.width / 2 - 65, Screen.width / 2 - 250, 100, 100), "Game Over", over_style);
                if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 170, 100, 50), "Restart"))
                {
                    action.Restart();
                    return;
                }
            }
        }
        else
        {
            GUI.Label(new Rect(Screen.width / 2 - 40, Screen.width / 2 - 320, 100, 100), "Archery", over_style);
            if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 170, 100, 50), "Game Start"))
            {
                game_start = true;
                action.BeginGame();
            }
        }
    }
}

我们的场记以及接口的实现如下所示:

public class FirstSceneController : MonoBehaviour, IUserAction, ISceneController
{
    public Camera mainCamera;
    public Camera childCamera;
    public ScoreRecorder recorder;
    public ArrowFactory arrowFactory;
    public ArrowFlyActionManager actionManager;
    public GameObject bow;
    private GameObject arrow;
    private GameObject target;
    private int[] targetscore = { 15, 30, 40, 50 };
    private int round = 0;
    private int arrowNum = 0;      

    private List<GameObject> store = new List<GameObject>();

    private bool gameOver = false;
    private bool gameStart = false;
    private string wind = "";
    private float windDirectX;      
    private float windDirectY;

    void Start()
    {
        SSDirector director = SSDirector.GetInstance();
        arrowFactory = Singleton<ArrowFactory>.Instance;
        recorder = Singleton<ScoreRecorder>.Instance;
        director.CurrentScenceController = this;
        actionManager = gameObject.AddComponent<ArrowFlyActionManager>() as ArrowFlyActionManager;
        LoadResources();
        mainCamera.GetComponent<CameraFlow>().bow = bow;
        windDirectX = Random.Range(-1, 1);
        windDirectY = Random.Range(-1, 1);
        CreateWind();
    }

    void Update()
    {
        if (gameStart)
        {
            for (int i = 0; i < store.Count; i++)
            {
                GameObject temp = store[i];
                if (temp.transform.position.z > 35 || store.Count > 5)
                {
                    arrowFactory.FreeArrow(store[i]);
                    store.Remove(store[i]);
                }
            }
        }
    }

    public void LoadResources()
    {
        bow = Instantiate(Resources.Load("Prefabs/Bow", typeof(GameObject))) as GameObject;
        target = Instantiate(Resources.Load("Prefabs/Target", typeof(GameObject))) as GameObject;
    }

    public void MoveBow(float offsetX, float offsetY)
    {
        if (gameOver || !gameStart)
            return;
        if (bow.transform.position.x > 5)
        {
            bow.transform.position = new Vector3(5, bow.transform.position.y, bow.transform.position.z);
            return;
        }
        else if (bow.transform.position.x < -5)
        {
            bow.transform.position = new Vector3(-5, bow.transform.position.y, bow.transform.position.z);
            return;
        }
        else if (bow.transform.position.y < -3)
        {
            bow.transform.position = new Vector3(bow.transform.position.x, -3, bow.transform.position.z);
            return;
        }
        else if (bow.transform.position.y > 5)
        {
            bow.transform.position = new Vector3(bow.transform.position.x, 5, bow.transform.position.z);
            return;
        }

        offsetY *= Time.deltaTime;
        offsetX *= Time.deltaTime;
        bow.transform.Translate(offsetX, 0, 0);
        bow.transform.Translate(0, offsetY, 0);
    }

    public void Shoot()
    {
        if ((!gameOver || gameStart) && arrowNum <= 10)
        {
            arrow = arrowFactory.GetArrow();
            store.Add(arrow);
            Vector3 wind = new Vector3(windDirectX, windDirectY, 0);
            actionManager.ArrowFly(arrow, wind);
            childCamera.GetComponent<ChildCamera>().StartShow();
            recorder.arrowNumber--;
            arrowNum++;
        }
    }

    public void CheckGamestatus()
    {

        if (recorder.arrowNumber <= 0 && recorder.score < recorder.targetScore)
        {
            gameOver = true;
            return;
        }
        else if (recorder.arrowNumber <= 0 && recorder.score >= recorder.targetScore)
        {
            round++;
            arrowNum = 0;
            if (round == 4)
                gameOver = true;
            for (int i = 0; i < store.Count; ++i)
            {
                arrowFactory.FreeArrow(store[i]);
            }
            store.Clear();
            recorder.arrowNumber = 10;
            recorder.score = 0;
            recorder.targetScore = targetscore[round];
        }
        windDirectX = Random.Range(-(round + 1), (round + 1));
        windDirectY = Random.Range(-(round + 1), (round + 1));
        CreateWind();
    }

    public void CreateWind()
    {
        string Horizontal = "", Vertical = "", level = "";
        if (windDirectX > 0)
        {
            Horizontal = "west";
        }
        else if (windDirectX <= 0)
        {
            Horizontal = "east";
        }
        if (windDirectY > 0)
        {
            Vertical = "South";
        }
        else if (windDirectY <= 0)
        {
            Vertical = "North";
        }

        if ((windDirectX + windDirectY) / 2 > -1 && (windDirectX + windDirectY) / 2 < 1)
        {
            level = "LV1";
        }
        else if ((windDirectX + windDirectY) / 2 > -2 && (windDirectX + windDirectY) / 2 < 2)
        {
            level = "LV2";
        }
        else if ((windDirectX + windDirectY) / 2 > -3 && (windDirectX + windDirectY) / 2 < 3)
        {
            level = "LV3";
        }
        else if ((windDirectX + windDirectY) / 2 > -5 && (windDirectX + windDirectY) / 2 < 5)
        {
            level = "LV4";
        }

        wind = Vertical + Horizontal + " " + level;
    }

    public void BeginGame()
    {
        gameStart = true;
    }

    public bool GetGameover()
    {
        return gameOver;
    }

    public int GetScore()
    {
        return recorder.score;
    }

    public int GetTargetScore()
    {
        return recorder.targetScore;
    }

    public int GetResidueNum()
    {
        return recorder.arrowNumber;
    }

    public string GetWind()
    {
        return wind;
    }

    public void Restart()
    {
        gameOver = false;
        recorder.arrowNumber = 10;
        recorder.score = 0;
        recorder.targetScore = 15;
        round = 0;
        arrowNum = 0;
        for (int i = 0; i < store.Count; ++ i)
        {
            arrowFactory.FreeArrow(store[i]);
        }
        store.Clear();
    }

}

导演类的实现还是一样:

public class SSDirector : System.Object
{
    private static SSDirector _instance;                  //导演类的实例
    public ISceneController CurrentScenceController { get; set; }
    public static SSDirector GetInstance()
    {
        if (_instance == null)
        {
            _instance = new SSDirector();
        }
        return _instance;
    }
}

最后,为了美观效果,我们给我们的游戏添加一个简单的天空盒,可以直接到商店界面下载免费资源;为了让游戏更真实,我们在下方加入草地,模拟游戏的场地效果;最后游戏的界面效果如下:
在这里插入图片描述
当箭飞到离靶一定距离的时候,右下方就可以显示我们的子摄像机,让玩家可以清楚地看见自己射箭的结果如何,让他们清楚下一箭该如何调整位置。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值