Unity 如何写一个足球运动员AI(一)

本文由RoadLun原创,转载请声明

(此处的AI并不是阿尔法狗那种高大上的人工智能,仅只游戏中的电脑角色)

效果如下:
射门


传球:


追球:


1. 先设计好球员的状态,将球员模型详细的用图片表示出来,再设计球员的状态和逻辑时会事半功倍:


这是初步的球员模型设计,以后有需求再扩展吧。分析一下球员的状态:有踢球、射门、抢断、传球这种瞬间发动的状态,但以上操作如果面前没有球则无法执行,还包括:带球,跑位,追球,回防,这种持续一段时间的状态。而且身边有球时优先改变为有球的状态(带球,传球,射门等),其次当身边没有球时,考虑没有球的状态(追球,回防,跑位),还要考虑带球时被抢断后AI的反应。

2.然后我设计出这个思路:
球员类: 定义球员的属性,如:速度,踢球的力量,腿长(人高马大的球员能踢到更远的球),性格(进攻性选手有更高概率射门,辅助型选手有更高概率传球等等),并且球员类定义球员的各种方法,例如带球,射门,传球,跑位,回防等等。
球员状态管理类:决定球员处于什么状态,执行什么方法
看向球类:球员时刻看向球,看起来不至于太僵硬
球员类里写一个枚举,列出各种状态,再Update里写一个Swith Case,不同状态对应不同方法,如果是射门传球这种只执行一帧的状态,则只调一次方法。

3.把各种球员方法定义出来
下面是球员类源码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerStateCon : MonoBehaviour
{

    //这个脚本负责设置球员AI的属性:移速,冷却,力度,智慧,灵敏等
    //并且负责定义所有状态对应的方法方法,如射门,传球,带球,待传球,追球,抢断,回防,跑位等

    //球员属性
    public float speed;     //速度
    public float cooling;   //冷却
    public float smart;     //智能(做出正确选择的概率)
    public float sensitive; //灵敏(思考时间)
    public float power;     //力量
    private Vector3 attackDir;  //当前球员的进攻方向
    public Transform ballTran;
    Rigidbody ballRig;          //球的刚体
    public Transform blueShootDir;  //蓝方射门方向
    public Transform redShootDir;   //红方射门方向

    public enum Team
    {
        Blue = 0,
        Red = 1
    };
    public Team team;
    //球员状态枚举
    public enum PlayerState
    {
        //进攻
        Shoot = 0,                 //射门
        Dribbling = 1,             //带球           
        Pass = 2,                  //传球
        WatiePass = 3,              //待传
        //防守
        Steals = 10,               //抢断
        ReturnDefense = 11,        //回防
        //通用
        Chase = 20,               //追球
        RunPosition = 21,         //跑位
        KickForward, //向前踢
        //等待守门员开球
        Static = 30,              //静止              不写
        //开局开球              
        WaitInPosition = 40         //在固定位置等待      不写
        

    };
    public PlayerState playerState;     //当前球员状态

    private void Start()
    {
        ballRig = ballTran.GetComponent<Rigidbody>();  //获取刚体
    }
    private void Update()
    {
        PersistenceState();
    }
    //各种方法

    //根据当前状态执行(都是持续状态)
    //要处理的持续状态有:带球,追球,静止
    void PersistenceState()
    {
        switch (playerState)
        {
            case PlayerState.Shoot:
                Dribbling();
                break;
            case PlayerState.Chase:
                Chase();
                break;
            case PlayerState.Static:
                break;
        }
    }

    //精确射门,(如果方向不对,则不射门)
    //获取碰撞体半径
    public Transform colliderCenter;    //碰撞中心    这个是自己定义的 再球员前方设置一个Transform即可
    public float colliderRadius;    //碰撞半径
    public void Shoot()
    {

        //bool siShoot=false;  //是否射门
        //Rigidbody ballRig;
        //判断范围内是否有球
        //前方是否正确
        //条件满足 射门                         
        //获取半径内所有碰撞体,如果是球,则加一个力,力的方向是球员到球(可上移)

        if (!IsHoldingBall())
        {
            Debug.Log("没有持球");
            
            return;
        }
        //switch case性能比if else好
        switch (team)
        {
            case Team.Blue:
                if (Vector3.Dot(Vector3.left, transform.forward) > 0)
                {
                    // Debug.Log("方向不对1");
                    GetComponent<PlayerStateTrigger_Con>().MonitoringTrigger();
                    return;
                }
                break;
            case Team.Red:
                if (Vector3.Dot(Vector3.left, transform.forward) < 0)
                {
                    // Debug.Log("方向不对2");
                    GetComponent<PlayerStateTrigger_Con>().MonitoringTrigger();
                    return;
                }
                break;
        }
        //两个条件都通过,可以踢球了
        Vector3 kickVec = ballRig.transform.position - transform.position;
        ballRig.AddForce(kickVec.normalized * power);
        // Debug.Log("踢球了");
        //ballRig = null;     //将刚体置空
    }
    //盲目射门(直接射门)
    public void BlindShoot()
    {
        //红方向红方射门点射门
        //兰芳向兰芳射门点射门
        if (team==Team.Blue)
        {
            ballRig.AddForce((blueShootDir.position - ballTran.position).normalized * power);
        }
        else
        {
            ballRig.AddForce((redShootDir.position - ballTran.position).normalized * power);
        }
    }

    //带球
    //推着球往前走
    public void Dribbling()
    {
        //条件如果方向正确
        //如果球在前方
        //如果有球
        //推着球向前移动
       
        if (!IsHoldingBall())
        {
            //Debug.Log("没有持球");
            //Debug.Log( "状态触发组件  "+GetComponent<PlayerStateTrigger_Con>());
            GetComponent<PlayerStateTrigger_Con>().StateReset();  //状态重置
            return;
        }
        //switch case性能比if else好
        switch (team)
        {
            case Team.Blue:
                if (Vector3.Dot(Vector3.left, transform.forward) > 0)
                {
                    // Debug.Log("方向不对1");
                    return;
                }
                break;
            case Team.Red:
                if (Vector3.Dot(Vector3.left, transform.forward) < 0)
                {
                    // Debug.Log("方向不对2");
                    return;
                }
                break;
        }
        //条件符合,推着球向前走,方向为当前方向,角度为插值
        Debug.Log("正在带球");
        transform.position += -transform.right * speed*Time.deltaTime;
        //Debug.Log("正在带球");
    }

    //传球
    //如果前方有队友,且与队友的角度不超过60°(全角),
    //且射出一个射线,射线没有击中对方球员,则传球
    public float findTeammateSphereRaidus;      //用来找队友的球的半径
    Transform teammateTran;
    public void Pass()
    {
        if (!IsHoldingBall())
        {
            Debug.Log("没有持球");
            return;
        }

        //先获得队友信息
        Collider[] allColls = Physics.OverlapSphere(transform.position, findTeammateSphereRaidus);
        foreach (Collider item in allColls)
        {
            if ( team==Team.Blue&& item.tag == "BlueNPC")
            {
                teammateTran = item.transform;
                break;      //这一步表示,不论有多少个队友,只查找集合的第一个,并返回
            }
            if (team==Team.Red&&item.tag=="RedNPC")
            {
                teammateTran = item.transform;
                break;
            }
        }
        if (teammateTran==null)
        {
            Debug.Log("没找到队友");
            GetComponent<PlayerStateTrigger_Con>().MonitoringTrigger();
            return;
        }
        //判断角度
       // float angle=Vector3.Angle((teammateTran.position - transform.position), -transform.right);
       // if (angle>60&&angle<-60)
       // {
       //     Debug.Log("角度不对");
       //     GetComponent<PlayerStateTrigger_Con>().MonitoringTrigger();
       //     return;
       // }
        Ray ray = new Ray(transform.position, (teammateTran.position - transform.position));


        
        RaycastHit hit;
        
        if (Physics.Raycast(ray,out hit,Mathf.Infinity))    //射一条无穷大的射线
        {
            //如果射线碰到对方选手,则返回
            if (team==Team.Blue&&hit.collider.tag=="RedNPC")
            {
                Debug.Log("蓝方选手射线射到红方选手");
                GetComponent<PlayerStateTrigger_Con>().MonitoringTrigger();
                return;
            }
            if (team==Team.Red&&hit.collider.tag=="BlueNPC")
            {
                Debug.Log("红方选手射线射到蓝方选手");
                GetComponent<PlayerStateTrigger_Con>().MonitoringTrigger();
                return;
            }
        }
        else
        {
            //Debug.Log("射线有问题");
            //GetComponent<PlayerStateTrigger_Con>().MonitoringTrigger();
            //return;
        }
        //条件达成 可以传球
        Debug.Log("传球 "+gameObject.name);
        ballRig.AddForce((teammateTran.position - transform.position).normalized * power);

        teammateTran = null;  //执行完,必定置空
    }

    //等待传球
    void WaitPass()
    {
        //指令 原地等待
        //注视球飞过来 (后期再解决旋转问题,目前先不写)
        
    }

    //抢断
    void Steals()
    {
        //抢断:用来干扰对方进攻,
        //如果球在范围内,并且如果当前面向地方大门,则踢一脚
        if (!IsHoldingBall())
        {
            Debug.Log("球不在范围内,无法抢断");
            return;
        }
        if (team==Team.Blue&&Vector3.Dot(-transform.right,Vector3.left)<0)
        {
            //蓝队 方向错了,返回
            Debug.Log("蓝队队员 方向错误,无法抢断");
            return;
        }
        if (team==Team.Red&&Vector3.Dot(-transform.right,Vector3.left)>0)
        {
            Debug.Log("红队队员 方向错误,无法抢断");
            return;
        }
        //条件通过 可以抢断
        ballRig.AddForce((ballTran.position - transform.position).normalized * power);
        Debug.Log("抢断成功");              
    }

    //回防
    public void ReturnDefense()
    {
        //当对手持球进攻时
        //当前角色回防,以规定速度向
        if (team==Team.Blue)
        {
            transform.position += Vector3.left * speed * Time.deltaTime;
            //Debug.Log("正在回防");
        }
        else
        {
            transform.position += Vector3.right * speed * Time.deltaTime;
        }

    }

    //追球
    public void Chase()
    {   
        //追逐球
        transform.position += (ballTran.position - transform.position).normalized * speed*Time.deltaTime;
       // Debug.Log("正在追球");
    }

    //跑位
    //根据球的位置判断跑向哪个方向,此处写成瞬发方法而非状态方法,
    //因为如果跑位的时候球的动向转变,但角色还是再跑位,显得很蠢
    //左右移动
    public float runPositionForce;
    public void RunPosition()
    {
        int a = Random.Range(0, 1);
        if (a==0)
        {
            GetComponent<Rigidbody>().AddForce(-transform.right * runPositionForce);
        }
        else
        {
            GetComponent<Rigidbody>().AddForce(transform.right * runPositionForce);
        }
        //瞬发方法生效,重置状态;
       // GetComponent<PlayerStateTrigger_Con>().StateReset();
    }

    //向前踢
    //当球进入触发范围时,球员有概率强前踢,如果时红方,就像Vector3.right方向踢,
    //如果蓝方就像Vector3.left方向踢
    public void KickForward()
    {
        //Debug.Log("")
        if (team==Team.Blue)
        {
            ballRig.AddForce(Vector3.left * power);
        }
        else
        {
            ballRig.AddForce(Vector3.right * power);
        }
    }


    //判断当前对象是否持球,如果没有持球,则不能进行带球,传球,运球
    bool IsHoldingBall()
    {
        return (colliderCenter.position - ballTran.position).magnitude < colliderRadius;    //球距离没有超过持球半径,默认持球(treu)
    }
}


以上有部分代码被我注掉了,以后用的时候再拿。
4.AI需要一点随机的元素,不然千篇一律真是很无趣,一种常见的作法是:给AI一个思考时间,思考时间是一个限定范围内的随机值,再思考时间内AI不会做任何事。随机的情况越多,情况将变得越不可预测,也会越有趣。再次我用到随机的地方是:球员接触到球的时候做出的反应是随机的,可能会射门,可能会带球,也可能传球,可能带球后传球/射门。球员没有接触到球时候,状态也是随机的,可能跑位,可能追球,可能回防。这种状态会持续一个规定范围内的随机时间。
综上所述,这应该够随机了吧   ~。~

下面是球员状态控制类源码:

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

public class PlayerStateTrigger_Con : MonoBehaviour {

    //触发状态和控制状态切换的脚本
    //触发优先级 > 非触发优先级
    //触发时最常见状态:射门和传球,其次带球
    //非触发时最常见状体:追球,其次跑位,静止

    //触发:实时判断球是否到踢范围内,如果到了,执行触球方法(根据概率选择触球方法),
    //非触发:用计时器确定一段时间的状态(计时器时间也是一个随机值),用一个随机数来修改状态

    //判断触球和非触球的枚举
    //enum 

    //用来判断球是否到达触发位置
    Transform colliderCenter;      //获取触发中心
    float colliderRadius;          //获取半径
    Transform ballTran;            //球的transform
    public float dribblingTimer=0f;  //带球计时器
    float notHoldStateTimer=0f;     //普通状态切换时间
    float coolingTimer=2;             //冷却计时
    float cooling;                  //射门冷却;
    
    //一段非触发状态的持续时间,随机(0.5,3)秒

    private void Start()
    {
        colliderCenter = GetComponent<PlayerStateCon>().colliderCenter;
        colliderRadius = GetComponent<PlayerStateCon>().colliderRadius;
        ballTran = GetComponent<PlayerStateCon>().ballTran;
        cooling = GetComponent<PlayerStateCon>().cooling;
        Debug.Log("初始冷却时间" + coolingTimer);
    }
    private void Update()
    {
        //控制冷却,  可删除**************************************
        //if (coolingTimer>0)   //说明正在冷却,开始计时
        //{
        //    coolingTimer -= Time.deltaTime;
        //   // Debug.Log("计时器时间: " + coolingTimer);
        //}
        //*****************************************************

        //如果开始带球,则开始计时且不触发任何方法
        if (dribblingTimer>0)
        {
            dribblingTimer -= Time.deltaTime;
            //正在带球,且只能带球
            return;
        }
        else
        {
            MonitoringTrigger();
        }
        //执行这一步,判断是否再带球,如果再带球,则直接返回
        if (dribblingTimer>0)
        {
            return;
        }
        else     //肯定没有再带球
        {
            notHoldStateTimer -= Time.deltaTime;
            if (notHoldStateTimer<=0)
            {
                RandomNotTriggerState();
            }
        }
        

        
        //如果没有触发
    }
    
    //状态重置方法,当某些情况没有状态的时候,进行状态重置。
    public void StateReset()
    {
        RandomNotTriggerState();
        Debug.Log("状态重置了");
    }
    public bool MonitoringTrigger()
    {
       // Debug.Log("出发了!!!!!  哇卡卡卡卡");   *****************************
       // if (coolingTimer >= 0)    //计时器正在冷却中,返回
       // {
       //     Debug.Log("计时器大于0,正在冷却");
       //     return false;
       // }                         ************************************************
        //Debug.Log("触碰到球了");
        if ((colliderCenter.position-ballTran.position).magnitude<colliderRadius)
        {
            //到范围内了 触发 随机一下
            float a = Random.Range(0f, 1f);
            //Debug.Log("触球时随机数: " + a);
            if (a>=0.8)
            {
                //向前踢
                Debug.Log("向前踢");
                GetComponent<PlayerStateCon>().playerState = PlayerStateCon.PlayerState.KickForward;
                GetComponent<PlayerStateCon>().KickForward();
            }
            else if (a>=0.6)
            {
                Debug.Log("射门");
                //射门
                //当前对象状态改为射门
                GetComponent<PlayerStateCon>().playerState = PlayerStateCon.PlayerState.Shoot;
                //调用射门方法
                GetComponent<PlayerStateCon>().BlindShoot();
            }
            else if(a>=0.2)
            {
                Debug.Log("传球");
                //传球
                //修改状态为传球
                GetComponent<PlayerStateCon>().playerState = PlayerStateCon.PlayerState.Pass;
                //调用传球方法
                GetComponent<PlayerStateCon>().Pass();
            }
            else    //0.2的概率
            {
                Debug.Log("带球");
                //带球
                //改变状态为带球
                GetComponent<PlayerStateCon>().playerState = PlayerStateCon.PlayerState.Dribbling;
            }
            //触发了
            coolingTimer = cooling;     //执行方法后,计时器
            return true;
        }
        return false;
    }
    void RandomNotTriggerState()  //通过这个方法,生成随机数,根据随机数调整
    {
        notHoldStateTimer = Random.Range(0.5f, 3f);
        float a= Random.Range(0f, 1f);
        //Debug.Log("非触发状随机数为: " + a);
        if (a>=0.2)      //40%概率去追球
        {
            Debug.Log("触发脚本:状态改变为追球: "+gameObject);
            GetComponent<PlayerStateCon>().playerState = PlayerStateCon.PlayerState.Chase;  //改变状态去追球,再PlayStateCon里面switch判断
        }
        else //40概率跑位  这是个瞬发方法
        {
            Debug.Log("触发脚本:状态改变为跑位");
            GetComponent<PlayerStateCon>().playerState = PlayerStateCon.PlayerState.RunPosition;//
            GetComponent<PlayerStateCon>().RunPosition();
        }
       
    }
}
okk~ 就是这样~

  • 15
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
### 回答1: 要在 Unity一个战斗 AI 逻辑,需要以下步骤: 1. 定义 AI 的状态:如待机、追踪、攻击等。 2. 编状态切换代码:比如当玩家进入 AI 的视野范围时,从待机状态切换到追踪状态。 3. 实现每个状态的行为:如在追踪状态下向玩家移动,在攻击状态下向玩家发射攻击。 4. 编敌人的行为树:行为树是一种图形化的表示 AI 行为的方法,可以帮助我们快速地编和调试 AI 逻辑。 5. 在游戏中测试你的 AI:运行游戏,观察 AI 的行为是否符合你的预期,并在必要时对 AI 逻辑进行修改。 这些步骤可以帮助你在 Unity 中实现一个简单的战斗 AI 逻辑。然而,在实际开发中,可能需要进行更多的编码工作,以实现更复杂的 AI 逻辑。 ### 回答2: Unity是一款流行的游戏开发引擎,可以帮助开发人员创建各种游戏。要一个战斗AI逻辑,我们需要考虑以下几个方面。 首先,我们需要定义敌人和玩家的属性,例如生命值、攻击力、防御力等。这些属性将决定战斗中的伤害和生存能力。 其次,我们需要为敌人编一个AI脚本。该脚本可以通过检测玩家的位置和状态来做出决策。例如,敌人可以根据玩家的距离选择不同的攻击方式。如果玩家距离过远,敌人可能会使用远程攻击;如果玩家接近,敌人可能会选择近身攻击。 我们还可以为敌人添加一些高级行为,如使用技能或躲避玩家的攻击。这些行为可以通过在AI脚本中编一些条件和动作来实现。例如,当敌人生命值低于一定阈值时,它可以选择使用一个强力技能来对玩家造成更大的伤害。 最后,我们需要在场景中放置敌人和玩家,并将AI脚本附加到敌人的游戏对象上。这样,当游戏运行时,敌人将根据AI脚本的逻辑来自动与玩家进行战斗。 综上所述,使用Unity战斗AI逻辑需要定义角色属性、编AI脚本以做出决策,并将脚本附加到相应的游戏对象中。 ### 回答3: 编战斗AI逻辑需要考虑到敌方角色的行为和敌我双方的互动模式。首先,我们可以设定敌方角色的基本行为,如进攻、防守、逃避等。接下来,需要制定AI的决策树,包括选择攻击目标、选择攻击方式和实施防守等。在制定这些决策时,需要考虑敌方角色的状态、能力和战术。 为了实现战斗AI逻辑,可以使用Unity中的协程或者状态机来处理敌方角色的行为。首先,我们可以使用一个协程控制敌方角色的行动,这可以确保敌方角色在每一帧都能够对自己的状态进行更新。在每一帧中,可以根据敌方角色当前的状态和目标选择合适的行动。比如,如果目标处于攻击范围内,则选择攻击;如果目标超出攻击范围,则选择追逐;如果没有目标,则可以选择巡逻或者待机。 另外一种实现方式是使用状态机。我们可以定义敌方角色的不同状态,如追逐状态、攻击状态、防守状态等。敌方角色在不同的状态下有不同的行为逻辑。通过不同状态之间的切换,敌方角色能够根据战斗情况做出相应的决策。 在编战斗AI逻辑时,还需要考虑到敌我双方的互动。我们可以为敌我双方的角色设置触发条件,如敌我距离、敌我血量比例等。根据这些触发条件,可以进行合理的判断和决策。比如,当我方血量较低时,敌方角色会主动进攻;当敌我距离过远时,敌方角色会选择追击等。 总之,编战斗AI逻辑需要考虑到敌方角色的行为、敌我双方的互动以及使用协程或状态机来实现。通过合理的逻辑设计和算法实现,能够让游戏的战斗AI更加智能和具有挑战性。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值