unity实现跟屁虫案例、克隆小人案例、简易的炮塔保卫战案例

789 阅读9分钟

1.导入资源包

image.png

2.配置player人物

1.添加动画器

在assets文件中新建动画控制器,将空对象idle和run动作拖拽到动画窗格中,并创建好俩个之间的过渡

新建一个变量isRun,类型为bool,用来控制是否可以跑

image.png

并将两个的过渡时间取消勾选

2.添加胶囊碰撞器

给胶囊配置器设置好与人物之间的高度,

3.添加导航网格代理

在这里面可以配置人物移动的速度,角速度(就是人物在移动中的旋转的角度)、加速度(就是一次速度到下一次速度之间的速度差就是加速度)、取消自动刹车功能

4.编写player脚本文件

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

public class player : MonoBehaviour
{
    // 
    public NavMeshAgent nav;
    public GameObject secondPlayer;
    Animator ani;
    public GameObject endPoint;
    void Start()
    {
        nav = GetComponent<NavMeshAgent>();
        ani = GetComponent<Animator>();

        //一开始就走向终点
        nav.SetDestination(endPoint.transform.position);
    }

    // Update is called once per frame
    void Update()
    {
        /*if (nav.velocity != Vector3.zero) ani.SetBool("isRun", true);
        else ani.SetBool("isRun", false);*/


        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        bool res = Physics.Raycast(ray, out hit);

        if (res && Input.GetMouseButton(0))
        {
            nav.SetDestination(hit.point);
        }
        if (nav.velocity != Vector3.zero)
        {
            ani.SetBool("isRun", true);

        }
        else
        {
            ani.SetBool("isRun", false);
        }

        //按下空格结束动画
        if (Input.GetKeyDown(KeyCode.Space))
        {
            nav.isStopped = true; //表示停止人物的移动
        }

        if (nav.isStopped)
        {
            //如果玩家1停下来了,玩家2就要停下
            secondPlayer.GetComponent<NavMeshAgent>().isStopped = true;
        }


    }
    private void OnControllerColliderHit(ControllerColliderHit hit)
    {
        //加了角色控制器,可以使用这个方法来判断是否碰撞到了--不用胶囊碰撞器,且另一个不用加上刚体
    }
}

3.配置player人物

配置player2与配置player类似,都是需要添加动画器,但是在这里需要注意的是,俩个玩家实现追踪的效果的话,第二个玩家的动画器是不能个第一个玩家的动画器使用一样的,所以在player2我们需要单独放置一个新的动画控制器来操作,(直接复制一个新的playerControl即可同样是通过bool类型的isRun来控制奔跑)

添加导航代理窗格、胶囊碰撞器、以及编写player2的脚本文件

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

public class player2 : MonoBehaviour
{
    //玩家2要关联玩家1
    public GameObject firstPlayer; //实时获取玩家的坐标
    NavMeshAgent nav;
    Animator ani;
    void Start()
    {
        nav = GetComponent<NavMeshAgent>();
        ani = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        nav.SetDestination(firstPlayer.transform.position);
        //让玩家播放移动的动画,根据速度来判断知道玩家动了
        //Vector3.zero表示物体在三个方向的速度
        if (nav.velocity != Vector3.zero) //因为velocity是一个vector3类型的值,判断为0应该是(0,0,0)
        {
            //当玩家要移动的时候判断速度是否为0,如果是为0,则不让他动,为0就是要动
            ani.SetBool("isRun", true);

        }else
        {
            ani.SetBool("isRun", false);
        }

        
    }
}

思路: 在这个项目中,是通过在player脚本文件公开绑定player2,将player2当做预制体拖入到player声明的脚本文件中,同时player2脚本文件中也是通过这种方式绑定和player的方式

类似下面这样:

image.png

image.png

俩者进行绑定后,我们就可以实现,当玩家一移动的时候,玩家2可以获取到玩家1的地址,并移动到玩家1的当前地址;当玩家1停止的时候,我们同时在玩家1的脚本文件中通过获取玩家2的导航代理将当前的导航停止,也就是下面的代码

//按下空格的时候停下
    if (Input.GetKeyDown(KeyCode.Space))
        {
            nav.isStopped = true; //表示停止人物的移动
        }

        if (nav.isStopped)
        {
            //如果玩家1停下来了,玩家2就要停下
            secondPlayer.GetComponent<NavMeshAgent>().isStopped = true;
        }

4.效果如下:

跟屁虫.gif

二.克隆小人

1.新建startPoint空对象点位,并将脚本文件getEnemy挂载到点位startPoint中

将开始点位放置到要克隆的本物体身上,保持在同一个平面,(可以直接赋值平面y轴的值进行设置)

编写getEnemy脚本文件中

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

public class getEnemy : MonoBehaviour
{
    public GameObject enemy;
    //敌人的数量
    int num = 20;
    //生成敌人的时间
    float time = 0.5f;
    void Start()
    {
        StartCoroutine(getEnemies());
    }

    IEnumerator getEnemies()
    {
        while (true)
        {
            Instantiate(enemy, transform.position, transform.rotation);
            num--;
            if (num <= 0) yield break;
            yield return new WaitForSeconds(time);
        }
        
    }
}

2.player作为预制体

player作为预制体拖入到enemy对象中,删除player,将终点endPoint点位拖入到player预制体身上,

image.png

3.编写player脚本文件

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

public class player : MonoBehaviour
{
    // 
    public NavMeshAgent nav;
    public GameObject secondPlayer;
    Animator ani;
    public GameObject endPoint;
    void Start()
    {
        nav = GetComponent<NavMeshAgent>();
        ani = GetComponent<Animator>();

        //一开始就走向终点
        nav.SetDestination(endPoint.transform.position);
    }

    // Update is called once per frame
    void Update()
    {
        if (nav.velocity != Vector3.zero) ani.SetBool("isRun", true);
        else ani.SetBool("isRun", false);

    }

}

效果如下:

克隆小人.gif

三、简易的炮塔保卫战案例

1.player作为预制体

player作为预制体,挂载player的脚本文件,player脚本文件实现如下:

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

public class player : MonoBehaviour
{
    // 
    public NavMeshAgent nav;
    public GameObject secondPlayer;
    Animator ani;
    public GameObject endPoint;
    float speed;
    void Start()
    {
        nav = GetComponent<NavMeshAgent>();
        ani = GetComponent<Animator>();

        //一开始就走向终点
        nav.SetDestination(endPoint.transform.position);
        nav.speed = Random.Range(2, 5);
    }

    // Update is called once per frame
    void Update()
    {
        if (nav.velocity != Vector3.zero) { 
            
            ani.SetBool("isRun", true); 
        }
        else ani.SetBool("isRun", false);

    }
}

2.空对象startPoint

在startPoint空对象上挂载getEnemy脚本文件,实现的是克隆多个敌人出来,一直往终点endPoint走去。

getEnemy脚本文件如下:

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

public class getEnemy : MonoBehaviour
{
    public GameObject enemy;
    //敌人的数量
    int num = 20;
    //生成敌人的时间
    float time = 2f;
    void Start()
    {
        StartCoroutine(getEnemies());
    }

    IEnumerator getEnemies()
    {
        while (true)
        {
            Instantiate(enemy, transform.position, transform.rotation);
            num--;
            if (num <= 0) yield break;
            yield return new WaitForSeconds(time);
        }
        
    }
}

3.floor地板

将floorControl脚本文件挂载到每一个地板上,并且将Tower塔作为预制体拖入Tower变量中,实现的是,当我们点击地板的时候,我们会触发一个点击方法,在这个OnMouseUpAsButton方法里面我们将Tower作为孩子插入到当前点击的floor地板下。

    private void OnMouseUpAsButton()
    {
        //Debug.Log("当前触发了");
        //克隆炮塔  要判断当前floor有没有炮塔
        if(transform.childCount == 0)
        {
            //判断孩子数量是不是0,为0就可以创建炮塔
            GameObject clone = Instantiate(tower, transform.position, transform.rotation);
            //塔的父亲是当前floor
            //设置塔的父亲为当前floor
            clone.transform.parent = transform;
        }
    }

floorControl脚本文件如下:

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

public class floorControl : MonoBehaviour
{
    //关联炮塔
    public GameObject tower;
    void Start()
    {
        //点击地板,出现炮塔
    }

    //当我点击碰撞体的时候会触发该方法
    private void OnMouseUpAsButton()
    {
        //Debug.Log("当前触发了");
        //克隆炮塔  要判断当前floor有没有炮塔
        if(transform.childCount == 0)
        {
            //判断孩子数量是不是0,为0就可以创建炮塔
            GameObject clone = Instantiate(tower, transform.position, transform.rotation);
            //塔的父亲是当前floor
            //设置塔的父亲为当前floor
            clone.transform.parent = transform;
        }
    }
}

同时当我们点击的时候,会引入Tower类型的变量,并且我们在Tower预制体中绑定了Tower脚本文件,所以在点击的时候同时,不但能生成塔,也能触发Tower脚本文件的代码

Tower脚本文件如下:

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

public class tower : MonoBehaviour
{
    // 关联塔顶
    public GameObject towerTop;
    //关联出炮点
    public GameObject firePoint;
    //关联火焰效果
    public GameObject fire;
    //创建一个碰撞目标
    public Collider target;
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        //判断有没有目标  有目标需要旋转炮塔
        Debug.Log(target);
        if (target != null)
        {
            Debug.Log("触发了");   
            //旋转塔顶
            towerTop.transform.LookAt(new Vector3(target.transform.position.x,
                towerTop.transform.position.y, target.transform.position.z));
        }
    }
    //打印当前触发的敌人
    //思路:当前有敌人进来会更新当前的目标target,当敌人离开的时候清空目标,设置为null
    private void OnTriggerEnter(Collider enemy) //如果敌还没有离开攻击范围,就不更新目标
    {
        
        //当前没有目标则给它一个目标
        if (target == null && enemy.tag == "Player")
        {
            Debug.Log(enemy.name);
            target = enemy;
            //旋转塔顶
            towerTop.transform.LookAt(new Vector3(target.transform.position.x,
                towerTop.transform.position.y, target.transform.position.z));
            shoot();
        }

    }
    //当敌人离开的时候
    private void OnTriggerExit(Collider enemy)
    {
        if (target != null) target = null;
    }

    //第三个方法 持续碰撞
    //监听当前面一个敌人会null的时候,会自动将目标转化为下一个enemy
    private void OnTriggerStay(Collider enemy)
    {
        if (target == null && enemy.tag == "Player")
        {
            //target.transform.position.x
            target = enemy;
            towerTop.transform.LookAt(new Vector3(target.transform.position.x,
                towerTop.transform.position.y, target.transform.position.z));
            shoot();
        }
    }

    //发射炮火 可能有时间间隔比如说每0.5秒发射一个
    public void shoot()
    {
        //Debug.Log("开始攻击");
        GameObject clone = Instantiate(fire, firePoint.transform.position, Quaternion.identity);
        clone.transform.LookAt(target.transform.position);
    }
}

在Tower脚本文件实现的是,关联了塔顶、出炮点、火焰效果。当人物经过到塔的范围内,会触发碰撞效果,有碰撞就会有对应的碰撞方法实现,并且要勾选是否为触发器

前面说过,碰撞体会默认阻挡刚体的运动,但是有些时候需要检测两个物体发生重叠但又不想引起物理上的碰撞,就需要勾选此选项,将碰撞体变成一个触发器
当勾选此项后,该物体就不会再阻挡刚体运动了,但当有刚体与当前物体发生重叠的时候,会调用 以下三个方法

// 自己(勾选了)开始接触另一个(勾选了)触发
    void OnTriggerEnter(Collider other)
  // 自己(勾选了)停止接触另一个(勾选了)触发
    void OnTriggerExit(Collider other)
     // 自己(勾选了)一直接触另一个(勾选了)不断触发
    void OnTriggerStay(Collider other) 

image.png

Tower脚本文件:

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

public class tower : MonoBehaviour
{
    // 关联塔顶
    public GameObject towerTop;
    //关联出炮点
    public GameObject firePoint;
    //关联火焰效果
    public GameObject fire;
    //创建一个碰撞目标
    public Collider target;
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        //判断有没有目标  有目标需要旋转炮塔
        Debug.Log(target);
        if (target != null)
        {
            Debug.Log("触发了");   
            //旋转塔顶
            towerTop.transform.LookAt(new Vector3(target.transform.position.x,
                towerTop.transform.position.y, target.transform.position.z));
        }
    }
    //打印当前触发的敌人
    //思路:当前有敌人进来会更新当前的目标target,当敌人离开的时候清空目标,设置为null
    private void OnTriggerEnter(Collider enemy) //如果敌还没有离开攻击范围,就不更新目标
    {
        
        //当前没有目标则给它一个目标
        if (target == null && enemy.tag == "Player")
        {
            Debug.Log(enemy.name);
            target = enemy;
            //旋转塔顶
            towerTop.transform.LookAt(new Vector3(target.transform.position.x,
                towerTop.transform.position.y, target.transform.position.z));
            shoot();
        }

    }
    //当敌人离开的时候
    private void OnTriggerExit(Collider enemy)
    {
        if (target != null) target = null;
    }

    //第三个方法 持续碰撞
    //监听当前面一个敌人会null的时候,会自动将目标转化为下一个enemy
    private void OnTriggerStay(Collider enemy)
    {
        if (target == null && enemy.tag == "Player")
        {
            //target.transform.position.x
            target = enemy;
            towerTop.transform.LookAt(new Vector3(target.transform.position.x,
                towerTop.transform.position.y, target.transform.position.z));
            shoot();
        }
    }

    //发射炮火 可能有时间间隔比如说每0.5秒发射一个
    public void shoot()
    {
        //Debug.Log("开始攻击");
        GameObject clone = Instantiate(fire, firePoint.transform.position, Quaternion.identity);
        clone.transform.LookAt(target.transform.position);
    }
}

当我们加载Tower脚本文件的时候,关联了火预制体,同时火作为预制体挂载了fireControl脚本文件,在脚本文件我们实现了火的发射,也就是火的克隆,并且火碰撞地板、碰撞人的时候对应的业务实现

fireControl脚本文件

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

public class fireControl : MonoBehaviour
{
    //给火有个速度
    float speed;
    // 一旦火被激活了就要往前移动
    void Start()
    {
        //当每一个游戏对象都被激活后,到开始的生命周期的时候会执行
        speed = Random.Range(1, 5); //发射移动速度是随机的
    }

    // Update is called once per frame
    void Update() //this.gameObject获取的是当前游戏对象 一个是当前的脚本
    {
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }
    //打到敌人,如果敌人离开了攻击范围,火如果打到敌人就消灭掉
    //火超出了打击范围,碰到敌人就消失,判断标签是否Player,碰到地板就消失

    private void OnTriggerEnter(Collider other)
    {
        Debug.Log($"火:{other.tag}");
        if (other.tag == "plane") //
        {
            Destroy(this.gameObject); //移除当前本身火
        }
        else if (other.tag == "Player")
        {
            Destroy(this.gameObject); //移除当前本身火
            //敌人消失
            Destroy(other.gameObject);  
        }
    }
}


这里需要特别注意的是:

火身上也有碰撞器,这时候我们一定要勾选是触发器选项,这样我们攻击到人物的时候不会阻碍人物的前进,并且我们也触发对应的方法实现人物消失和火消失的业务

实现最终效果如下:

塔防.gif

这里也有一些问题,就是塔在开始的位置,每一个人物一出来的话就会被塔攻击,在这里我们需要调快一些人物克隆的速度和数量,并且火的发射速度也需要慢一些,不然人物都到达不了终点,这样游戏也没有玩的任何意义了,所以需要改善一下个别地方即可。