Unity 3D游戏编程自学#6——简单射击案例

时间:2024-04-04 20:16:27

游戏简介:一个只能移动枪口的射击游戏,敌人会在前方场景随机生成,需要玩家在20秒的时间内尽可能多地开枪击杀。

1.枪支随鼠标移动

先导入网上找到的场景模型、枪支模型(网上找来的免费资源,感谢网友!),如图:

Unity 3D游戏编程自学#6——简单射击案例

然后对枪编写脚本:

Unity 3D游戏编程自学#6——简单射击案例

为方便就不事先获得枪支的Transform引用,直接gameObject.GetComponent调用Transform组件下的LookAt方法,该方法可使枪向射线与场景碰撞点处调整方向,因此事先要将为场景模型添加Mesh Collider,也因此在游戏测试时如果将鼠标指向天空等无碰撞体的地方,枪支则不会移动。(可通过在背景处创建一个蓝色的Plane当作天空来解决此问题)

Unity 3D游戏编程自学#6——简单射击案例

在为场景模型添加碰撞体时,有时候会有多种碰撞体可选,因为这里我们要碰撞体仅仅是为了得到一个碰撞点令枪可以移动,所以可以选择简单一些的碰撞体模型,一般碰撞体的复杂程度可由上图中的verts(顶点)来确定,顶点越少,计算量越小,游戏运行越流畅。

注意:测试时可能会发现枪支随鼠标的旋转很不自然(绕着枪中心处旋转),现实中应该是绕着射手的肩膀旋转,因此应将旋转中心设置在枪的后部,这可以通过创建空物体以及父子关系来调整。因此以上脚本应该挂载到作为父物体的空物体上:

Unity 3D游戏编程自学#6——简单射击案例

 

2. 枪支红点效果

在gun父物体下再创建一个空物体RedPoint,位置设置在枪口(因为红光是从枪口射出的,不是从枪中间射出的),并为其添加Line Renderer组件,设置好其颜色、宽度。(具体步骤上文“线特效”处有提到)

然后补充脚本:

private Ray ray;
private RaycastHit hit;
private Transform gunTransform;
private Transform pointTransform;
private LineRenderer rayLineRenderer;
void Start()
{
    gunTransform = gameObject.GetComponent<Transform>();
    pointTransform = gunTransform.FindChild("RedPoint");
    rayLineRenderer = pointTransform.gameObject.GetComponent<LineRenderer>();
}
void Update()
{
    ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    if (Physics.Raycast(ray,out hit))
    {
        gunTransform.LookAt(hit.point);
        rayLineRenderer.SetPosition(0, pointTransform.position);
        rayLineRenderer.SetPosition(1, hit.point);
    }
}

这里要通过gun下面的子物体RedPoint生成红光,但脚本是挂载在gun上的,因此要通过FindChild方法得到RedPoint及其LineRenderer组件的引用。

最后通过SetPosition绘制红线,这里有两个参数,第一个是射线经过的路程上的拐点的索引,第二个是位置。脚本中设置了两个拐点(其实就是一条线段,如果有三个不在同一直线上的拐点,则红线由两条线段组成),最终效果:

Unity 3D游戏编程自学#6——简单射击案例

 

3.敌人生成与击倒

导入僵尸模型,调整其大小,并为其及其所有零部件(子物体)的标签设置为Zombie。

因为模型本身没有自带碰撞体组件,需要手动添加,因为本人使用的模型过于复杂(还有动画效果),因此仅对父物体添加一个胶囊型的碰撞体(覆盖其头部与身体)。考虑到其动画会使其向前稍微移动,因此碰撞体的位置靠前一些。

Unity 3D游戏编程自学#6——简单射击案例

然后编写脚本:(接在rayLineRenderer.SetPosition后面)

if (hit.collider.tag == "Zombie" && Input.GetMouseButtonDown(0))
{
    zombieTransform = hit.collider.gameObject.GetComponent<Transform>();
    zombieTransform.Rotate(Vector3.left, 90);
    GameObject.Destroy(hit.collider.gameObject, 2);
}

当向鼠标指向处发射的射线碰撞到的是僵尸,且按下鼠标左键(开火),则首先获取到僵尸的Transform组件的引用,然后旋转(击倒效果),并在2秒后销毁该物体。

如果有较好的模型,可对模型的所有零部件(子物体)添加碰撞体,则脚本应该为:(接在rayLineRenderer.SetPosition后面)

if (hit.collider.tag == "Zombie" && Input.GetMouseButtonDown(0))
{
    Transform zombieParent = hit.collider.gameObject.GetComponent<Transform>().parent;
    Transform[] zombie = zombieParent.GetComponentsInChildren<Transform>();
    for (int i = 0; i < zombie.Length; i++)
    {
        zombie[i].gameObject.AddComponent<Rigidbody>();
    }
    zombieParent.Rotate(Vector3.left, 90);
    GameObject.Destroy(zombieParent.gameObject, 2);
}

因为碰撞到的是模型的子物体,所以通过.parent得到父物体的Transform组件,进而通过GetComponentsInChildren得到所有零部件的Transform组件,最后通过循环对所有子物体添加Rigidbody,这样一来,僵尸会“四分五裂”,一方面因为重力,另一方面是因为各个子物体间的碰撞体有重合,添加Rigidbody后各部分会自发散开。

接下来新建一个实例化僵尸的脚本:

public GameObject prefabZombie;
float i = 0;
void Update()
{
    if (i>1)
    { 
        Vector3 position = new Vector3(Random.Range(-35,0),2.6f,Random.Range(-10,-5));
        GameObject.Instantiate(prefabZombie, position, Quaternion.identity);
        i = 0;
    }
    i += Time.deltaTime;   
}

在合适的区域每秒生成一个僵尸,这里用累加Time.deltaTime的方式实现每秒执行,也可以用Invoke函数实现。

为了方便管理生成的僵尸,我们可以将所有生成的僵尸设置为一个空物体的子物体:

public GameObject prefabZombie;
public Transform zombieControler;
void Start()
{
    zombieControler = gameObject.GetComponent<Transform>();
    InvokeRepeating("Creat", 1, 1);
}
void Creat()
{
    Vector3 position = new Vector3(Random.Range(-35, 0), 2.6f, Random.Range(-10, -5));
    GameObject.Instantiate(prefabZombie, position, Quaternion.identity);
    newZombie.GetComponent<Transform>().SetParent(zombieControler);
}

用SetParent方法即可实现,最后将该脚本挂载到空物体ZombieControler上即可。

 

4. 音效及界面UI

添加开枪音效:

private AudioSource gunSound;
void Start()
{
    gunSound = gameObject.GetComponent<AudioSource>();
}

...
if (Input.GetMouseButtonDown(0))
{
    gunSound.Play();
    if (hit.collider.tag == "Zombie")
    {
        ...
    }
}

然后为挂载以上脚本的枪支模型添加Audio Source组件,选择合适的开枪音效即可。(注意不要勾选Play On Awake和Loop)

接下来创建UI界面,先将游戏分为三个状态:开始、游戏中以及结束,并编辑对应的UI(创建空物体,添加GUIText组件并调整文字位置、大小、颜色),以下是UI叠加在一起的Game界面:

Unity 3D游戏编程自学#6——简单射击案例                    Unity 3D游戏编程自学#6——简单射击案例 

开始界面要有游戏玩法的基本介绍以及“开始游戏”的按钮,游戏中需要显示分数以及剩余时间,游戏结束需要分数统计、“重新开始”按钮和“退出游戏”按钮。

接下来对父物体UI,创建脚本:

首先编辑一个控制游戏状态的方法(这里采用枚举类型,主要是为了直观,要我写可能就用0、1、2来表示了)。

并将UI在某状态是否显示进行规定:(类似的就省略不写了)

public enum GameState { Start, Gaming, End }
private GameObject startUI;
...

void Start()
{
    startUI = GameObject.Find("Start");
    ...
    ChangeState(GameState.Start);
}

public void ChangeState(GameState state)
{
    if (state==GameState.Start)
    {
        startUI.SetActive(true);
        gamingUI.SetActive(false);
        endUI.SetActive(false);
        ...
    }
    if (state == GameState.Gaming)
    {
        ...
    }
    if (state == GameState.End)
    {
        ...
    }
}

这里需要注意的是ChangeState要为public,因为其他脚本的一些操作也会改变游戏状态。(例如点击开始游戏按钮)

接下来编辑“开始游戏”按钮,以下脚本挂载在“StartSign”:

private UIContorl uiContorl;
void Start()
{
    uiContorl = GameObject.Find("UI").GetComponent<UIContorl>();
}

void OnMouseDown()
{
    uiContorl.ChangeState(UIContorl.GameState.Gaming);
}

首先查找到父物体UI挂载的脚本中的ChangeState方法,只要MouseDown就改变游戏状态。

同理可设置重新开始按钮和退出按钮,其中退出按钮极为简单:

void OnMouseDown()
{
    Application.Quit();
}

 

5. 分数、时间显示及部分细节

当在游戏开始和结束状态时,枪支无法移动,因此要对Move脚本进行编辑:

public void ChangeGunMove(bool state)
{
    f = state;
}

void Update()
{
    if (f)
    {
        ...
    }
}

然后在ChangeState方法中的对应阶段设置state为true或false即可。

开始和结束阶段僵尸也无法生成,因此Invoke不能写在Start里面,更改CreateZombie脚本:(以下为CreateZombie脚本的完整版)

Unity 3D游戏编程自学#6——简单射击案例

在ChangeState的Gaming状态下调用CreateZombie即可,另外,在游戏结束时,除了要CancelInvoke,还要把玩家未消灭的僵尸清除。

接下来要在Gaming中显示分数,在UIControl脚本中添加:

public void AddScore()
{
    score++;
    scoreText.text = "击杀数:"+score;
}

然后在Move脚本中添加:

if (hit.collider.tag == "Zombie")
{
    uiContorl.AddScore();
    ...
}

每次进入到Gaming状态,要重置分数,在结束状态也要再显示一次分数:

if (state == GameState.Gaming)
{
    score = 0;
    scoreText.text = "击杀数:" + score;
    ...
}
if (state == GameState.End)
{
    finalScore.text = "最终击杀数:" + score;
    ...
}

接下来显示剩余时间,在UIControl脚本中添加:

void Update()
{
    if (startTime)
    {
        time -= Time.deltaTime;
        timeText.text = "剩余时间:" + Math.Round(time, 2);
        if (time < 0)
        {
            ChangeState(GameState.End);
            startTime = false;
        }
    } 
}

startTime为bool类型,因为Update时刻运行,因此添加该变量确保倒计时只在Gaming状态下运行。

if (state == GameState.Gaming)
{
    startTime = true;
    time = 20;
    ...
}

至此全部脚本编写完毕。

 

6. 效果及源代码

Game界面:

Unity 3D游戏编程自学#6——简单射击案例

Build后运行结果:(分别是开始界面、游戏界面、结束界面)

Unity 3D游戏编程自学#6——简单射击案例

Unity 3D游戏编程自学#6——简单射击案例

Unity 3D游戏编程自学#6——简单射击案例

原谅我懒得细调开始和结束UI的位置……

最后放出Move脚本:

Unity 3D游戏编程自学#6——简单射击案例

UIControll脚本:

Unity 3D游戏编程自学#6——简单射击案例

Unity 3D游戏编程自学#6——简单射击案例

最后提醒:

做项目前一定要仔细规划(游戏阶段划分、资源模型的管理、脚本,尤其是各种变量名编写一定要注意,我在写的时候差点被自己编的变量名绕晕),此外还有脚本中各个变量的访问修饰符(public还是private一定要搞清楚)。

实际上,如果是个人开发,变量全写public也可以,但这并不是什么好习惯,尤其是在日后团队协作全写public就是坑队友嘛,所以建议现在开始就养成好习惯,细究public和private。

 

本文部分内容来自擅码网(http://www.mkcode.net)Unity 3D课程,经本人学习、整理得来,若有错漏,欢迎指正!