本文由RoadLun原创,转载请声明
(此处的AI并不是阿尔法狗那种高大上的人工智能,仅只游戏中的电脑角色)
效果如下:
射门
![](https://i-blog.csdnimg.cn/blog_migrate/1dbe2217ed9125ece66f67d420f4166c.gif)
传球:
![](https://i-blog.csdnimg.cn/blog_migrate/f74445e3866a0f64477482eb272bac72.gif)
追球:
![](https://i-blog.csdnimg.cn/blog_migrate/1ae34f5c241bc78ec5cee17061ec696d.gif)
1.
先设计好球员的状态,将球员模型详细的用图片表示出来,再设计球员的状态和逻辑时会事半功倍:
![](https://i-blog.csdnimg.cn/blog_migrate/fd2ec78b141c57e8b488e9f054e3f7d2.png)
这是初步的球员模型设计,以后有需求再扩展吧。分析一下球员的状态:有踢球、射门、抢断、传球这种瞬间发动的状态,但以上操作如果面前没有球则无法执行,还包括:带球,跑位,追球,回防,这种持续一段时间的状态。而且身边有球时优先改变为有球的状态(带球,传球,射门等),其次当身边没有球时,考虑没有球的状态(追球,回防,跑位),还要考虑带球时被抢断后AI的反应。
2.然后我设计出这个思路:
球员类: 定义球员的属性,如:速度,踢球的力量,腿长(人高马大的球员能踢到更远的球),性格(进攻性选手有更高概率射门,辅助型选手有更高概率传球等等),并且球员类定义球员的各种方法,例如带球,射门,传球,跑位,回防等等。
球员状态管理类:决定球员处于什么状态,执行什么方法
看向球类:球员时刻看向球,看起来不至于太僵硬
球员类里写一个枚举,列出各种状态,再Update里写一个Swith Case,不同状态对应不同方法,如果是射门传球这种只执行一帧的状态,则只调一次方法。
![](https://i-blog.csdnimg.cn/blog_migrate/4e6233d5a8ec1904248a600979e6a6a3.png)
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~ 就是这样~