3D游戏建模与设计6:基于Unity平台编写游戏:打飞碟(clay pigeon shooting)

1.游戏简介

  在美国,有一种游戏叫飞碟射击(clay pigeon shooting),又称为打飞碟。它是一种用枪械射击抛在空中作为飞靶的圆形黏土碟的射击运动。英文中对飞碟的称呼是“黏土鸽子”(clay pigeon)或“鸟”(bird),原因是过去英国的射击比赛曾使用活鸽子作为靶子,虽然在1921年被立法禁止而改用陶碟模拟飞鸟,但叫法还是保留了下来,把命中目标叫“打死”(kill)、没能击中叫“鸟逃了”(bird away)、发射飞碟的机械叫“鸟阱”(trap)。  

  我们用游戏模拟现实世界,本次游戏就模拟现实世界中的飞碟射击运动,并将这一运动进行简化,我们不再利用枪支打飞碟,而是利用鼠标我们就可以进行飞碟射击运动,鼠标点击飞碟可使飞碟消失,模拟真实世界中子弹击中飞碟的效果,现在点击开始游戏按钮并进行游玩吧。

2.游戏规则

  1)玩家打到红色飞碟得1分,打到绿色飞碟得2分,打到蓝色飞碟得3分 。

  2)一共发射五轮飞碟,对于第n轮,会发射2的n-1次方个飞碟,例如第一轮发射1个飞碟,第二轮发射2个飞碟,第三轮发射4个飞碟,以此类推,当所有飞碟发射完毕时游戏结束。

3.游戏玩法

  使用鼠标点击飞镖,点击不同颜色的飞碟就有相应的得分,为了得到更高的分数,玩家需要在有限的轮数内尽力打到最多的飞碟,此外,轮次数越大,发射的飞碟数更多,玩家一次性无法打中所有飞碟,故玩家要选择得分高的颜色的飞碟打,这无疑增加了游戏的可玩性。

4.游戏UML图

MainControllor为主控制器,用于加载资源、开始游戏、重启游戏、判断游戏是否结束等。

IUserAction:用户动作基类接口,内含有用户能通过GUI进行的操作。

UserGUI:用户控制相关的GUI等。

CCFlyAction:主要用于回调,当飞碟飞出屏幕时进行回调。

ISSActionCallbck:实现回调函数。

SceneControllor:用于控制场景。

 SSAction:动作基类接口。

SSActionManger:动作管理者基类并不直接作为动作管理者使用,它的存在是为调用者提供较简单和通用的接口。

SSDirector:场记。

DiskFactory:以工厂模式控制飞碟。

DiskData:飞碟基础数据。

CCActionManager:具体动作作为类中的一个对象从而管理动作。

5.游戏编写

  本游戏基于Unity引擎编写,使用的系统为Windows10,使用的脚本文件语言为C#,首先需要确保保证电脑中安装Unity引擎,我的版本是2023.3.8f1c1,还需下载VSCode或VStudio用于编写代码,还需要Unity的Plastic SCM用于管理代码,具体安装方法这里不过多介绍。

接着我们打开Unity,找到项目,选择新项目。

 选择3D,项目命名为clay pigeon shooting,保存地址我选择的是D盘。(启动版本管理可选可不选)。

现在我们进入了Unity引擎界面,如图所示,什么内容也没有。

本项目中运用到的代码、文件非常多,为了保证界面的简洁性,建议在Assests项目栏中如下图建

立几个文件夹方便管理。

建立方法如下:在Assets栏中空白区用鼠标右键点击,选择Create中的Folder生成我们需要的游戏文件。

然后我们先制作飞碟,先在Resources文件夹中建立文件夹Prefabs。

然后在文件夹Prefabs制作飞碟,在左侧SampleScene中右键鼠标,选择3D Object->Cylinder

将Cylinder命名为Disk_Prefab,我的飞碟各个参数设置如下,也可以自行设置飞碟。

然后在Scripts文件夹中编写代码,共有以下几个脚本:

我们先编写UserGUI脚本,UerGUI主要是设置Label和button,代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UserGUI : MonoBehaviour
{
    private IUserAction userAction;
    public string gameMessage;
    public int points;
    bool gameStart = false; //判断是否在游戏中

    void Start()
    {
        points = 0;
        gameMessage = "";
        userAction = SSDirector.GetInstance().CurrentScenceController as IUserAction;
        gameStart = false;
    }


    void OnGUI()
    {
        //小字体初始化
        GUIStyle style = new GUIStyle();
        style.normal.textColor = Color.white;
        style.fontSize = 30;

        //大字体初始化
        GUIStyle bigStyle = new GUIStyle();
        bigStyle.normal.textColor = Color.white;
        bigStyle.fontSize = 50;
        
        if (!gameStart)
        {
            GUI.Label(new Rect(Screen.width * 0.5f - 195, Screen.height * 0.25f, 100, 100), "Clay Pigeon Shooting", bigStyle);
            if (GUI.Button(new Rect(Screen.width * 0.5f - 50, Screen.height * 0.5f, 100, 40), "开始游戏"))
            {
                gameStart = true;
                userAction.GameStart(gameStart);
            }
        }
        if(gameStart)
        {
           
            GUI.Label(new Rect(Screen.width * 0.5f - 50, 20, 100, 30), "得分: " + points, style);
            GUI.Label(new Rect(Screen.width * 0.5f - 50, Screen.height * 0.5f - 200, 100, 100), gameMessage, style);
            userAction.SetMode(false);
            if (GUI.Button(new Rect(20, 50, 100, 40), "Restart"))
            {
                userAction.Restart();
            }
            if (Input.GetButtonDown("Fire1"))
            {
                userAction.Hit(Input.mousePosition);
            }
        }
    }
}

我们现在编写IUserAction脚本,用户有三个动作,代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface IUserAction
{
    void Hit(Vector3 position);
    void Restart();
    void GameStart(bool Mystart);
}

我们现在编写DiskData,飞碟主要有speed、points和direction三个属性,代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DiskData : MonoBehaviour
{
    public float speed;         //水平速度
    public int points;          //得分
    public Vector3 direction;   //初始方向
}

我们现在编写DiskFactory脚本,该脚本主要用于生成飞碟与释放飞碟,其中GetDisk用于生产飞碟,我们先从free列表中查找是否有空闲的飞碟,如果没有则另外创建一个新飞碟。为了确保难度是由简单到复杂,及低轮次数只产生分数比较小的飞碟(红色),高轮次数产生分数比较大的飞碟(蓝色与绿色),我们给每个飞碟设置等级,飞碟的等级与轮次数有关,我们在0~2之间的随机数乘以轮次数获得飞碟的等级,并按照等级设置飞碟的分数、颜色、速度、方向等基本信息,这样就实现了我们的目的。而FreeDisk用于释放飞碟,将飞碟从used列表中移除并添加到free列表中以供下一轮的使用。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DiskFactory : MonoBehaviour
{
    public GameObject disk_Prefab;              //飞碟预制

    private List<DiskData> used;                //正被使用的飞碟
    private List<DiskData> free;                //空闲的飞碟

    public void Start()
    {
        used = new List<DiskData>();
        free = new List<DiskData>();
        disk_Prefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Disk_Prefab"), Vector3.zero, Quaternion.identity);
        disk_Prefab.SetActive(false);
    }

    public GameObject GetDisk(int round)
    {
        GameObject disk;
        //如果有空闲的飞碟,则直接使用,否则生成一个新的
        if (free.Count > 0)
        {
            disk = free[0].gameObject;
            free.Remove(free[0]);
        }
        else
        {
            disk = GameObject.Instantiate<GameObject>(disk_Prefab, Vector3.zero, Quaternion.identity);
            disk.AddComponent<DiskData>();
        }

        //按照round来设置飞碟属性
        //不同等级数对应不同颜色的飞镖,当等级数为:
        //0~4是红色飞碟  
        //4~7是绿色飞碟  
        //7~10是蓝色飞碟
        //飞碟的等级 = 0~2之间的随机数 * 轮次数
        float level = UnityEngine.Random.Range(0, 2f) * (round + 1);
        if (level < 4)//红色飞碟
        {
            disk.GetComponent<DiskData>().points = 1;
            disk.GetComponent<DiskData>().speed = 4.0f;
            disk.GetComponent<DiskData>().direction = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
            disk.GetComponent<Renderer>().material.color = Color.red;
        }
        else if (level > 7)//绿色飞碟
        {
            disk.GetComponent<DiskData>().points = 3;
            disk.GetComponent<DiskData>().speed = 8.0f;
            disk.GetComponent<DiskData>().direction = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
            disk.GetComponent<Renderer>().material.color = Color.blue;
        }
        else//蓝色飞碟
        {
            disk.GetComponent<DiskData>().points = 2;
            disk.GetComponent<DiskData>().speed = 6.0f;
            disk.GetComponent<DiskData>().direction = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
            disk.GetComponent<Renderer>().material.color = Color.green;
        }

        used.Add(disk.GetComponent<DiskData>());

        return disk;
    }

    public void FreeDisk(GameObject disk)
    {
        //找到使用中的飞碟,将其踢出并加入到空闲队列
        foreach (DiskData diskData in used)
        {
            if (diskData.gameObject.GetInstanceID() == disk.GetInstanceID())
            {
                disk.SetActive(false);
                free.Add(diskData);
                used.Remove(diskData);
                break;
            }

        }
    }
}

我们现在编写Singleton脚本,Singleton为场景单实例类,当所需的实例第一次被需要时,在场景内搜索该实例,下一次使用时不需要搜索并直接返回,具体代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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;
        }
    }
}

我们现在编写SSAction脚本,在动作分离版本中,将动作其抽离为一个 SSAction 类,当这个物体需要进行某些动作时,再实时进行动作的创建,并进行Update()。并且为其定义一个 destroy 属性,当动作完成后自动将这个动作实例标记为可以删除的对象,等待游戏引擎自动回收这个对象,减少对系统资源的占用。具体代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SSAction : ScriptableObject
{
    public bool enable = true;
    public bool destroy = false;

    public GameObject gameObject { get; set; }
    public Transform transform { get; set; }
    public ISSActionCallback callback { get; set; }

    protected SSAction()
    {

    }

    // Start is called before the first frame update
    public virtual void Start()
    {
        throw new System.NotImplementedException();
    }

    // Update is called once per frame
    public virtual void Update()
    {
        throw new System.NotImplementedException();
    }
}

我们现在编写ISSActionCallBack脚本,当一个动作完成之后,我们需要通知其他的部分(如动作管理器或其他的动作)当前的动作已经完成,可以进行下一步的操作。因此添加一个SSActionCallbackInterface 类,使得在动作完成之后再进行一个回调操作,实现通知其他部分的这个功能。具体代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum SSActionEventType : int { Started, Competed }
public interface ISSActionCallback
{
    //回调函数
    void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Competed,
        int intParam = 0,
        string strParam = null,
        Object objectParam = null);
}

我们现在编写CCFlyAction脚本,CCMoveToAction 继承于 SSAction,并给动作管理器提供了GetSSAction的接口。当动作管理器调用这个方法的时候,可以直接生成一个动作的脚本,此时只需要将这个脚本赋予给某个物体,并由动作管理器进行动作的调度之后,即可产生物体做出这个动作的效果。在CCFlyAction脚本中,我们将飞碟的抛物线运动分解为水平方向运动与垂直方向运动,水平速度恒定,垂直方向施加重力加速度。此外,当飞碟到达底部时,动作结束,将进行回调。具体代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCFlyAction : SSAction
{
    float gravity;          //重力加速度
    float speed;            //水平速度
    Vector3 direction;      //飞行方向
    float time;             //时间

    //生产函数(工厂模式)
    public static CCFlyAction GetSSAction(Vector3 direction, float speed)
    {
        CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction>();
        action.gravity = 9.8f;
        action.time = 0;
        action.speed = speed;
        action.direction = direction;
        return action;
    }

    public override void Start()
    {
        
    }

    public override void Update()
    {
        time += (Time.deltaTime);
        transform.Translate(Vector3.down * gravity * time * Time.deltaTime);
        transform.Translate(direction * speed * Time.deltaTime);
        //如果飞碟到达底部,则动作结束,进行回调
        if (this.transform.position.y < -6)
        {
            this.destroy = true;
            this.enable = false;
            this.callback.SSActionEvent(this);
        }
        
    }
}

我们现在编写SSActionManager脚本,SSActionManager就是此次动作管理器实现的核心部分,其实现了对所有动作的管理,接收新创建的动作对象,并进行对已经存在的动作对象进行调度的功能,最后会将被标记为可删除的动作对象进行回收操作。在SSActionManager脚本中,我们用动作集、等待集与删除集管理动作,每次程序运行update()函数时先将waitingAdd中的动作保存,再运行被保存后的动作,运行后销毁该动作。对于下一次要执行的动作,先将动作初始化,然后保存在waitingAdd中,具体代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SSActionManager : MonoBehaviour
{
    //动作集,以字典形式存在
    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
    //等待被加入的动作队列(动作即将开始)
    private List<SSAction> waitingAdd = new List<SSAction>();
    //等待被删除的动作队列(动作已完成)
    private List<int> waitingDelete = new List<int>();

    protected void Update()
    {
        //将waitingAdd中的动作保存
        foreach (SSAction ac in waitingAdd)
            actions[ac.GetInstanceID()] = ac;
        waitingAdd.Clear();

        //运行被保存的事件
        foreach (KeyValuePair<int, SSAction> kv in actions)
        {
            SSAction ac = kv.Value;
            if (ac.destroy)
            {
                waitingDelete.Add(ac.GetInstanceID());
            }
            else if (ac.enable)
            {
                ac.Update();
            }
        }

        //销毁waitingDelete中的动作
        foreach (int key in waitingDelete)
        {
            SSAction ac = actions[key];
            actions.Remove(key);
            Destroy(ac);
        }
        waitingDelete.Clear();
    }

    //准备运行一个动作,将动作初始化,并加入到waitingAdd
    public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback manager)
    {
        action.gameObject = gameObject;
        action.transform = gameObject.transform;
        action.callback = manager;
        waitingAdd.Add(action);
        action.Start();
    }

    // Start is called before the first frame update
    protected void Start()
    {

    }

}

我们现在编写CCActionManger脚本,由于动作管理器的设计不局限于一个项目的使用,因此在这个项目中继承 SSActionManager 再设计一个具体的CCActionManger类,使得其可以在这个项目中产生具体的动作实例,再进行调度操作。CCActionManger脚本负责生成飞行动作,并接受飞行动作的回调信息,使飞碟被回收具体代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCActionManager : SSActionManager, ISSActionCallback
{
    //飞行动作
    public CCFlyAction flyAction;
    //控制器
    public FirstController controller;

    protected new void Start()
    {
        controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
        controller.actionManager = this;
    }

    public void Fly(GameObject disk, float speed, Vector3 direction)
    {
        flyAction = CCFlyAction.GetSSAction(direction, speed);
        RunAction(disk, flyAction, this);
    }

    //回调函数
    public void SSActionEvent(SSAction source,
    SSActionEventType events = SSActionEventType.Competed,
    int intParam = 0,
    string strParam = null,
    Object objectParam = null)
    {
        //飞碟结束飞行后进行回收
        controller.diskFactory.FreeDisk(source.gameObject);
    }
}

我们现在编写场景控制器SceneController脚本,具体代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface ISceneController
{
    void LoadResources();
}

现在我们编写SSDirector脚本,场记,用于加载场景,具体代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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;
    }
}

现在我们编写主控制器MainController脚本,MainController脚本负责游戏主要逻辑,现在具体介绍一下几个函数。

Start()函数:

  Start调用LoadReasources()函数。

LoadReasources()函数:

  LoadReasources用于各个参数初始化。

SendDisk()函数:
  SendDisk用于发射一个飞碟,首先从工厂获得一个飞碟,再为其设置初始位置和飞行动作。
Hit()函数:
  Hit用于处理用户的点击动作,将用户点击到的飞碟移除,并计算分数。

GameStart()函数:

  GameStart用于取得程序是否在游戏中的状态。
Restart()函数:
  Restart用于重置游戏。
Update()函数:
  Update用于发射飞碟与更新状态,飞碟每2s发射一次,每次做多3只,避免太过拥挤,当飞碟发射完毕后判断是否重置或者结束游戏。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
    public CCActionManager actionManager;                   //动作管理者
    public DiskFactory diskFactory;                         //飞碟工厂
    int[] roundDisks;           //对应轮次的飞碟数量
    bool isInfinite;            //游戏当前模式
    int points;                 //游戏当前分数
    int round;                  //游戏当前轮次
    int sendCnt;                //当前已发送的飞碟数量
    float sendTime;             //发送时间
    bool gameStart;

    void Start()
    {
        LoadResources();
    }

    public void LoadResources()
    {
        SSDirector.GetInstance().CurrentScenceController = this;
        gameObject.AddComponent<DiskFactory>();
        gameObject.AddComponent<CCActionManager>();
        gameObject.AddComponent<UserGUI>();
        diskFactory = Singleton<DiskFactory>.Instance;
        sendCnt = 0;
        round = 0;
        sendTime = 0;
        points = 0;
        isInfinite = false;
        roundDisks = new int[] { 1, 2, 4 ,8 ,16};
    }

    public void SendDisk()
    {
        //从工厂生成一个飞碟
        GameObject disk = diskFactory.GetDisk(round);
        //设置飞碟的随机位置
        disk.transform.position = new Vector3(-disk.GetComponent<DiskData>().direction.x * 7, UnityEngine.Random.Range(0f, 8f), 0);
        disk.SetActive(true);
        //设置飞碟的飞行动作
        actionManager.Fly(disk, disk.GetComponent<DiskData>().speed, disk.GetComponent<DiskData>().direction);
    }

    public void Hit(Vector3 position)
    {
        Camera ca = Camera.main;
        Ray ray = ca.ScreenPointToRay(position);

        RaycastHit[] hits;
        hits = Physics.RaycastAll(ray);

        for (int i = 0; i < hits.Length; i++)
        {
            RaycastHit hit = hits[i];
            if (hit.collider.gameObject.GetComponent<DiskData>() != null)
            {
                //将飞碟移至底端,触发飞行动作的回调
                hit.collider.gameObject.transform.position = new Vector3(0, -7, 0);
                //积分
                points += hit.collider.gameObject.GetComponent<DiskData>().points;
                //更新GUI数据
                gameObject.GetComponent<UserGUI>().points = points;
            }
        }
    }

    public void GameStart(bool Mystart)
    {
        gameStart = Mystart;
    }

    public void Restart()
    {
        gameObject.GetComponent<UserGUI>().gameMessage = "";
        round = 0;
        sendCnt = 0;
        points = 0;
        gameObject.GetComponent<UserGUI>().points = points;
    }

    public void SetMode(bool isInfinite)
    {
        this.isInfinite = isInfinite;
    }

    void Update()
    {
        if(gameStart)
        {
            sendTime += Time.deltaTime;
            //每隔1s发送一次飞碟
            if (sendTime > 2)
            {
                sendTime = 0;
                //每次发送至多3个飞碟
                for (int i = 0; sendCnt < roundDisks[round]; i++)
                {
                    sendCnt++;
                    SendDisk();
                }
                //判断是否需要重置轮次,不需要则输出游戏结束
                if (sendCnt == roundDisks[round] && round == roundDisks.Length - 1)
                {
                    if (isInfinite)
                    {
                        round = 0;
                        sendCnt = 0;
                        gameObject.GetComponent<UserGUI>().gameMessage = "";
                    }
                    else
                    {
                        gameObject.GetComponent<UserGUI>().gameMessage = "Game Over!";
                    }
                }
                //更新轮次
                if (sendCnt == roundDisks[round] && round < roundDisks.Length - 1)
                {
                    sendCnt = 0;
                    round++;
                }
            }
        }
    }
}

到此,所有脚本编写完毕,可以对游戏进行游玩了。

6.游戏演示

3D游戏编程与设计作业5 基于unity平台编写打飞碟游戏_哔哩哔哩bilibili

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
基于unity3d的动作角色扮演类游戏设计与实现需要考虑多个方面。首先,游戏中的角色动作要流畅自然,需要使用unity3d提供的动画系统来实现角色的移动、攻击、防御等动作。其次,游戏中的角色技能,装备系统也需要被设计和实现。玩家可以通过技能树来学习新的技能,通过装备系统来获取、升级、使用各种装备。 另外,游戏的场景设计也非常重要。使用unity3d的场景编辑器可以创建诸如城镇、森林、沙漠等多样化的地形,再通过光照、材质等技术手段来实现精美的场景效果,提高游戏的沉浸感和可玩性。此外,还需要考虑到角色的控制系统,玩家可以使用键盘、手柄等多种方式进行游戏操作,要保证操作的灵活性和舒适度。 在游戏设计方面,要考虑到游戏的主线剧情和支线任务的设计,以及与角色成长、奖励等方面的结合,来增加游戏的可玩性和吸引力。最后,还需要优化游戏性能,确保游戏在不同设备上都能流畅运行。 在实现方面,需要利用unity3d的编程语言(如C#)来进行游戏逻辑的编写和实现。同时可以使用unity3d提供的资源库,导入角色模型、动作、场景等资源,并控制它们在游戏中正确地展现和交互。 总之,基于unity3d的动作角色扮演类游戏设计与实现需要综合考虑角色动作、技能装备、场景设计、角色控制、游戏设计和性能优化等方面,同时充分利用unity3d提供的工具和资源库来实现游戏的各项功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值