unity3d 没有navMesh的AI坦克大战的实现
AI坦克大战
1.游戏简介:
> * 游戏目标:击毁地方基地。
> * AI行为:
1.拥有一定的视野范围。
2.若视野范围内出现敌方坦克,随机选择一个目标,旋转坦克机身,使坦克面朝目标并追踪敌方坦克。
3.目标进入攻击范围内,控制炮台瞄准目标并开火,击中率随机。
4.若视野范围内无任何敌方坦克,则向基地方向靠近。
5.基地进入攻击范围,且周围无敌方坦克时,攻击基地。
>*实现目标:做出给定坦克的AI模型,AI模型行为应与玩家控制的坦克行为较像。
>*点击下载完整项目资源
2.游戏预览
1.较平地面,有遮挡物情况演示:
2.陡峭地面坦克大战演示:
3.设计过程
1.寻找模型等游戏资源:
在Asset Store中搜索Kawaii_Tanks,下载项目。
在Asset Store中搜索Base,找到一个基地模型,下载。
打开里面的场景Base-01,有一个已经做好了的坦克,可以由玩家通过键盘,鼠标操纵。这次作业就利用这个作为模板,实现AI坦克。
2.我的AI实现思路:
让AI坦克判断当前所处状态,模拟用户输入,如键入方向键调整机身,模拟鼠标控制炮台运动,模拟按空格开火等。我个人觉得这是AI实现比较通用的做法,像这次这个坦克模型,就只能通过按方向键的方法实现移动,因为它没有动画,靠轮子驱动,其他实现移动的方法用起来坦克动的不正常。
3.实现AI的思路:
1.简单思路:AI坦克最基本的就是发现跟踪目标,自动位移,瞄准,开火这四个功能。但是如何实现这些功能呢?例如自动位移就有很多种实现方式,如直接修改transform,通过动画驱动物体位置变换,通过添加力或直接修改刚体的velocity,或者如这个坦克一般,通过给轮子加力矩,然后用转轴去带动坦克运行一般,通过纯物理方式进行。所以为了更好地实现AI坦克,首先就必须理解普通坦克四个功能的实现方法,并进行修改。
2.对普通坦克进行解析:
a.位移的实现:
仔细研究这个坦克的实现方法之后,我对原作者佩服得五体投地,坦克很简单的前进和转弯都是只利用方向键实现的,没有任何动画,而是通过给转动轮一个力矩让轮子转动,再用铰链hinge joint连接到一个类似转轴的东西上,转轴又通过hinge joint连接到主体上,带动主体运动。所以AI实现时只需根据需要,模拟键入方向键来控制坦克即可。AI坦克先模拟按左键或右键旋转机身面朝目标,然后再向前前进。
b.瞄准目标的实现:
普通坦克利用的是鼠标位置,炮台旋转到鼠标方向并调整垂直方向。在AI坦克中,可以直接把炮台旋转到目标位置附近,并随机调整炮台垂直方向,以实现随机命中。
b.开火的实现:
普通坦克利用的是键入空格键,坦克开火。在AI坦克中可以这样实现:判断一下,如果敌方坦克进入攻击范围,且炮台旋转到目标位置,则炮台通过BroadcastMessage向子对象炮口发送一个信号,炮口即可发射子弹。
3.一些附加功能的实现:
a.AI坦克自带视野:
如何实现“找到进入视野范围的敌方坦克”呢?
1.先通过findGameobjectsWithTag找到所有敌方坦克。
2.求出自身位置到目标位置的方向向量,目前坦克面朝的方向向量,利用Vector3.Angle()求出向量夹角。如果小于设定的视野角度,则可能在视野范围内。
3.从自身向目标发射一条射线,如果射线击中其他物体而非敌方坦克,说明两者之间阻碍物,此时敌方坦克不能算是“进入视野范围”。(更好的做法是向敌方坦克四个角都发射射线,若有一条射线击中坦克,则说明坦克进入视野范围)b.AI坦克旋转机身,使机身朝向目标:
b.旋转坦克机身使其面朝目标:
此处有两种做法,一种较复杂又傻逼,一种简单又省心(好吧,我用的是第一种,代码里也是第一种,就懒得改了)。
方法1(简单版):
利用localPositionOfTarget = this.transform.InverseTransformPoint(targetPosition),得出目标物体对于AI坦克的相对位置。(0,0,1)表示目标在AI坦克正前方一个单位处。此时可以判断,如果localPositionOfTarget.x > 0,则AI坦克应模拟按右键,向右旋转。否则,向左旋转。
方法2(脑缺版):
一开始不清楚有InverseTransformPoint这个函数,可以很方便地判断出目标位置在坦克面朝方向的第几象限,所以用了另一个方法。即分别求出transform.forward和两点间方向向量_dir,(利用Mathf.Atan),得到的是-180到180度的角,还需要进行处理为0--360度角,然后再分类讨论。。。
c.命中率随机的实现:
可以随机调整炮口的垂直方向角度,这个可以最方便地实现命中率随机。具体看AI_Cannon_Control的代码。有一些小细节是需要注意的,例如避免永远打不中对象的情况的发生。
d.如何防止高速小炮弹穿透物体而不引发碰撞事件:
在本例中应该并不存在此情况,因为炮弹够大且速度不是太快。但我还是考虑了这种情况。解决方法是,在FixedUpdate()中,从子弹向子弹前进方向发射一道射线,射线长度为一帧子弹运动距离,如果射线能够击中坦克,则判断出击中坦克,进行处理。同时,在CollisionEnter()中,也要判断一下有没有碰撞事件。可以加一些判断条件,避免被计算为两次碰撞事件。此方法可以解决穿透问题。
4.代码实现:
点击下载完整项目资源
1.基本方法:修改或重新编写所有与用户操作有关的控制脚本。
2.核心控制脚本为:
AI_ID_Control:该脚本实现角色控制,如果AI的ID设置为2,则会被自动控制。(原本的ID_Control版本为:如果ID为1,则坦克是由玩家操纵。)
AI_Wheel_Control:该脚本实现自动追踪视野范围内目标,为控制坦克运动的输入脚本。
AI_Wheel_Rotate:该脚本实现根据驱动轮子转动带动坦克运动。
AI_Turret_Control:该脚本实现炮台自动旋转到目标方向。
AI_Connol_Control:该脚本实现炮口垂直方向的瞄准,实现命中率随机。
AI_Fire_Control:实现特定条件下自动开火。
3.简单挂一下这6个脚本的代码,要理解坦克的运行,还需下载整个project,看一下AI坦克的预制。
AI_ID_Control.cs:
using UnityEngine; //using System.Collections; using System; using UnityStandardAssets.CrossPlatformInput ; // This script must be attached to Root Object of the tank. public class AI_ID_Control : MonoBehaviour { [ Header ( "ID number settings" ) ] [ Tooltip ( "ID number for this AI tank." ) ] public int myID = 2 ; int currentID = 2 ; void Start () { BroadcastMessage ( "Get_ID" , myID , SendMessageOptions.DontRequireReceiver ) ; BroadcastMessage ( "Get_Current_ID" , currentID , SendMessageOptions.DontRequireReceiver ) ; } void Update () { if ( Input.GetKeyDown ( KeyCode.Escape ) ) { Application.Quit () ; } #if UNITY_ANDROID || UNITY_IPHONE if ( CrossPlatformInputManager.GetButtonDown ( "Switch" ) ) { #else if ( Input.GetKeyDown ( KeyCode.Return ) ) { #endif ID_Control_CS [] iDScripts = FindObjectsOfType < ID_Control_CS > () ; int [] iDArray = new int [ iDScripts.Length ] ; for ( int i = 0 ; i < iDScripts.Length ; i++ ) { iDArray [ i ] = iDScripts [ i ].myID ; } Array.Sort ( iDArray ) ; for ( int i = 0 ; i < iDArray.Length ; i++ ) { if ( iDArray [ i ] == currentID ) { if ( i == iDArray.Length - 1 ) { currentID = iDArray [ 0 ] ; } else { currentID = iDArray [ i + 1 ] ; } break ; } } BroadcastMessage ( "Get_Current_ID" , currentID , SendMessageOptions.DontRequireReceiver ) ; } } }
AI_Wheel_Control.cs
using UnityEngine; using System.Collections; using UnityStandardAssets.CrossPlatformInput; using System.Collections.Generic; // This script must be attached to "MainBody". public class AI_Wheel_Control : MonoBehaviour { [ Header ( "Driving settings" ) ] [ Tooltip ( "Torque added to each wheel." ) ] public float wheelTorque = 2000.0f ; // Reference to "Wheel_Rotate". [ Tooltip ( "Maximum Speed (Meter per Second)" ) ] public float maxSpeed = 7.0f ; // Reference to "Wheel_Rotate". [ Tooltip ( "Rate for ease of turning." ) , Range ( 0.0f , 1.0f ) ] public float turnClamp = 0.5f ; [ Tooltip ( "Velocity the parking brake automatically works." ) ] public float autoParkingBrakeVelocity = 0.5f ; [ Tooltip ( "Lag time for activating the parking brake." ) ] public float autoParkingBrakeLag = 0.5f ; [ Tooltip ( "'Solver Iteration Count' of all the rigidbodies in this tank." ) ] public int solverIterationCount = 7 ; [ Tooltip ( "ange of fild eye sight" ) ] public float viewAngel = 70; //I add. [ Tooltip ( "target object if the tank can't find an enemy" ) ] public Transform originTarget; //I add. [ Tooltip ( "attack radius" ) ] public float attackRadius = 70; //I add. [HideInInspector] public float leftRate ; // Reference to "Wheel_Rotate". [HideInInspector] public float rightRate ; // Reference to "Wheel_Rotate". [HideInInspector] public Transform targetAITransform ; // Reference to "AI_Turret_Control" and "AI_Cannon_Control". [HideInInspector] public bool closeEnoughToShoot ; // Reference to "AI_Turret_Control" and "AI_Cannon_Control". Rigidbody thisRigidbody ; bool isParkingBrake = false ; float lagCount ; string thisTag; int myID ; bool isCurrentID = true ; void Awake () { // Layer Collision Settings. // Layer9 >> for wheels. // Layer10 >> for Suspensions. // Layer11 >> for MainBody. for ( int i =0 ; i <= 11 ; i ++ ) { Physics.IgnoreLayerCollision ( 9 , i , false ) ; // Reset settings. Physics.IgnoreLayerCollision ( 11 , i , false ) ; // Reset settings. } Physics.IgnoreLayerCollision ( 9 , 9 , true ) ; // Wheels do not collide with each other. Physics.IgnoreLayerCollision ( 9 , 11 , true ) ; // Wheels do not collide with MainBody. for ( int i =0 ; i <= 11 ; i ++ ) { Physics.IgnoreLayerCollision ( 10 , i , true ) ; // Suspensions do not collide with anything. } } void Start () { thisTag = this.transform.parent.gameObject.tag; if(thisTag == "myTank") { //if the tank is our friend originTarget = GameObject.FindGameObjectWithTag ("myTarget").transform.FindChild("MainBody"); //The base is with tag "myTarget"!! } else { originTarget = GameObject.FindGameObjectWithTag ("enemyTarget").transform.FindChild("MainBody"); //The base is with tag "enemyTarget"!! } Debug.Log(thisTag + "aim at " + originTarget.parent.gameObject.name); this.gameObject.layer = 11 ; // Layer11 >> for MainBody. thisRigidbody = GetComponent < Rigidbody > () ; thisRigidbody.solverIterations = solverIterationCount ; closeEnoughToShoot = false; BroadcastMessage ( "Get_Wheel_Control" , this , SendMessageOptions.DontRequireReceiver ) ; // Send this reference to all the "Wheel_Rotate". } void Update () { if ( isCurrentID ) { float vertical = 1; //上下键. float horizontal = 0; //左右键. List<Transform> listOfCandidate = new List<Transform>(); //store all enemy tank in the view of the tank. if (targetAITransform == null || (targetAITransform.parent.gameObject.tag != "myTank" && targetAITransform.parent.gameObject.tag != "enemyTank" )) { //if the tank is tracking nothing or the base. GameObject[] ArrAI; if (thisTag == "myTank") { ArrAI = GameObject.FindGameObjectsWithTag ("enemyTank"); //I am a my_tank, find all enemy_tanks! } else { ArrAI = GameObject.FindGameObjectsWithTag ("myTank"); } for (int i = 0; i < ArrAI.Length; i++) { //find all enemy tank Transform thisTransform = ArrAI [i].transform.FindChild ("MainBody"); //store tank in listOfCandidate Vector3 _dir = (thisTransform.position - transform.position).normalized; float angleBetweenDirAndFor = Vector3.Angle (transform.forward, _dir); // Debug.Log ("angle: " + angleBetweenDirAndFor); if (angleBetweenDirAndFor >= viewAngel) //out of filed of eyesight continue; RaycastHit hit; if (Physics.Raycast (transform.position, _dir, out hit)) { //the ray of these objects hit an object // Debug.Log("hit !!! " + hit.collider.gameObject.name); if (hit.rigidbody) { //eye sight is not blocked by other objects. if (hit.rigidbody.gameObject.name == "MainBody") { //hitted object is the tank! listOfCandidate.Add (thisTransform); } } } } ; if (listOfCandidate.Count > 0) { //random select a object targetAITransform = listOfCandidate [(Random.Range (0, listOfCandidate.Count))]; } else { targetAITransform = originTarget; } } Vector3 _direction = (targetAITransform.position - transform.position).normalized; Vector3 _forwardDir = (new Vector3 (transform.forward.x, 0, transform.forward.z)).normalized; //This part is to control the tank rotate or go ahead. float _dirAngle = Mathf.Atan2 (_direction.z, _direction.x) * Mathf.Rad2Deg; //0->180 -180->0 float _forAngle = Mathf.Atan2 (_forwardDir.z, _forwardDir.x) * Mathf.Rad2Deg; //0->180 -180->0 // Debug.Log ("_dirAngle is: " + _dirAngle + " forAngle is: " + _forAngle); _dirAngle = halfCircleToCircle(_dirAngle); //0->360 _forAngle = halfCircleToCircle (_forAngle); //0->360 if (close_enough_to_attack (transform.position, targetAITransform.position, attackRadius)) { //close closeEnoughToShoot = true; vertical = 0; horizontal = 0; //rotate the turret, then fire!! } else { //not close enough, go ahead or rotate. closeEnoughToShoot = false; if (getAbsoluteNumOfFloat (_dirAngle - _forAngle) < 20) { //close enought, go ahead vertical = 1; horizontal = 0; } else { if (_dirAngle >= 0 && _dirAngle <= 180) { if (_forAngle > _dirAngle && (_forAngle < _dirAngle + 180)) { //turn right vertical = 0; horizontal = Mathf.Clamp (1, -turnClamp, turnClamp); } else { //turn left vertical = 0; horizontal = Mathf.Clamp (-1, -turnClamp, turnClamp); } } else { float opositDirection = _dirAngle + 180; //oposite Direction of _dirAngle if (_forAngle >= _dirAngle || (_forAngle + 360) <= opositDirection) { //turn right vertical = 0; horizontal = Mathf.Clamp (1, -turnClamp, turnClamp); } else { //turn left; vertical = 0; horizontal = Mathf.Clamp (-1, -turnClamp, turnClamp); } } } } leftRate = Mathf.Clamp (-vertical - horizontal, -1.0f, 1.0f); rightRate = Mathf.Clamp (vertical - horizontal, -1.0f, 1.0f); } } bool close_enough_to_attack(Vector3 thisPosition, Vector3 targetPosition, float radius) { Vector3 diff = targetPosition - thisPosition; // Debug.Log ("radius: " + diff.sqrMagnitude); if (diff.sqrMagnitude < radius * radius) return true; else return false; } float halfCircleToCircle(float angle) { if (angle >= -180 && angle < 0) { return (angle + 360); } return angle; } float getAbsoluteNumOfFloat(float a) { if (a > 0.0f) return a; else return -a; } void FixedUpdate () { if ( leftRate == 0.0f && rightRate == 0.0f ) { float velocityMag = thisRigidbody.velocity.magnitude ; float angularVelocityMag = thisRigidbody.angularVelocity.magnitude ; if ( isParkingBrake == false ) { if ( velocityMag < autoParkingBrakeVelocity && angularVelocityMag < autoParkingBrakeVelocity ) { lagCount += Time.fixedDeltaTime ; if ( lagCount > autoParkingBrakeLag ) { isParkingBrake = true ; thisRigidbody.constraints = RigidbodyConstraints.FreezePositionX | RigidbodyConstraints.FreezePositionZ | RigidbodyConstraints.FreezeRotationY ; } } } else { if ( velocityMag > autoParkingBrakeVelocity || angularVelocityMag > autoParkingBrakeVelocity ) { isParkingBrake = false ; thisRigidbody.constraints = RigidbodyConstraints.None ; lagCount = 0.0f ; } } } else { isParkingBrake = false ; thisRigidbody.constraints = RigidbodyConstraints.None ; lagCount = 0.0f ; } } void Get_ID ( int idNum ) { myID = idNum ; } void Get_Current_ID ( int idNum ) { if ( idNum == myID ) { isCurrentID = true ; } else { isCurrentID = false ; } } }
using UnityEngine; using System.Collections; // This script must be attached to all the Driving Wheels. public class AI_Wheel_Rotate : MonoBehaviour { bool isLeft ; //left wheel or right wheel? Rigidbody thisRigidbody ; float maxAngVelocity ; AI_Wheel_Control controlScript ; Transform thisTransform ; Vector3 initialAng ; void Start () { this.gameObject.layer = 9 ; // Layer9 >> for wheels. thisRigidbody = GetComponent < Rigidbody > () ; // Set direction. if ( transform.localPosition.y > 0.0f ) { isLeft = true ; } else { isLeft = false ; } // Get initial rotation. thisTransform = transform ; initialAng = thisTransform.localEulerAngles ; } void Get_Wheel_Control ( AI_Wheel_Control tempScript ) { controlScript = tempScript ; float radius = GetComponent < SphereCollider > ().radius ; maxAngVelocity = Mathf.Deg2Rad * ( ( controlScript.maxSpeed / ( 2.0f * Mathf.PI * radius ) ) * 360.0f ) ; } void FixedUpdate () { float rate ; if ( isLeft ) { rate = controlScript.leftRate ; } else { rate = controlScript.rightRate ; } thisRigidbody.AddRelativeTorque ( 0.0f , Mathf.Sign ( rate ) * controlScript.wheelTorque , 0.0f ) ; thisRigidbody.maxAngularVelocity = Mathf.Abs ( maxAngVelocity * rate ) ; } void Update () { // Stabilize wheels. float angY = thisTransform.localEulerAngles.y ; thisTransform.localEulerAngles = new Vector3 ( initialAng.x , angY , initialAng.z ) ; } }
AI_Turret_Control.cs
using UnityEngine; using System.Collections; using UnityEngine.UI ; using UnityStandardAssets.CrossPlatformInput ; // This script must be attached to "Turret_Base". public class AI_Turret_Control : MonoBehaviour { [ Header ( "Turret movement settings" ) ] [ Tooltip ( "Maximum Rotation Speed. (Degree per Second)" ) ] public float rotationSpeed = 15.0f ; [ Tooltip ( "Time to reach the maximum speed. (Sec)" ) ] public float acceleration_Time = 0.2f ; [ Tooltip ( "Angle range for slowing down. (Degree)" ) ] public float bufferAngle = 5.0f ; Transform thisTransform ; Transform rootTransform ; float currentAng ; Vector3 targetPos ; Transform targetTransform ; // Vector3 targetOffset ; AI_Wheel_Control AI_Wheel_Control_Script; [HideInInspector] public Vector3 local_TargetPos ; // Reference to "Cannon_Control_CS". [HideInInspector] public Vector3 adjustAng ; // Reference to "Cannon_Control_CS". // Vector3 previousMousePos ; float speedRate ; #if UNITY_ANDROID || UNITY_IPHONE bool isButtonDown = false ; public float spherecastRasius = 2.5f ; #endif int myID ; bool isCurrentID = true ; void Start () { thisTransform = this.transform ; rootTransform = thisTransform.root ; currentAng = thisTransform.localEulerAngles.y ; AI_Wheel_Control_Script = thisTransform.GetComponentInParent < AI_Wheel_Control > (); targetPos = thisTransform.position + thisTransform.forward * 500.0f ; } void LateUpdate () { if ( isCurrentID ) { #if UNITY_ANDROID || UNITY_IPHONE Mobile_Control () ; #else Mouse_Control () ; #endif } } #if !UNITY_ANDROID && !UNITY_IPHONE void Mouse_Control () { ; } #endif #if UNITY_ANDROID || UNITY_IPHONE void Mobile_Control () { // Set Target. if ( CrossPlatformInputManager.GetButtonDown ( "Target_Press" ) ) { Cast_Ray_Mobile ( new Vector3 ( CrossPlatformInputManager.GetAxis ( "Target_X" ) , CrossPlatformInputManager.GetAxis ( "Target_Y" ) , 0.0f ) ) ; } // Control GunCam and Aiming. if ( CrossPlatformInputManager.GetButtonDown ( "GunCam_Press" ) ) { isButtonDown = true ; previousMousePos = new Vector3 ( CrossPlatformInputManager.GetAxis ( "GunCam_X" ) , CrossPlatformInputManager.GetAxis ( "GunCam_Y" ) , 0.0f ) ; BroadcastMessage ( "GunCam_On" , SendMessageOptions.DontRequireReceiver ) ; return ; } if ( isButtonDown && CrossPlatformInputManager.GetButton ( "GunCam_Press" ) ) { Vector3 currentMousePos = new Vector3 ( CrossPlatformInputManager.GetAxis ( "GunCam_X" ) , CrossPlatformInputManager.GetAxis ( "GunCam_Y" ) , 0.0f ) ; adjustAng += ( currentMousePos - previousMousePos ) * 0.02f ; previousMousePos = currentMousePos ; return ; } if ( CrossPlatformInputManager.GetButtonUp ( "GunCam_Press" ) ) { isButtonDown = false ; adjustAng = Vector3.zero ; BroadcastMessage ( "GunCam_Off" , SendMessageOptions.DontRequireReceiver ) ; } } #endif /* void Cast_Ray ( Vector3 screenPos , bool isLockOn ) { Ray ray = Camera.main.ScreenPointToRay ( screenPos ) ; RaycastHit raycastHit ; if ( Physics.Raycast ( ray , out raycastHit , 1000.0f ) ) { Transform colliderTransform = raycastHit.collider.transform ; if ( colliderTransform.root != rootTransform ) { if ( isLockOn ) { if ( raycastHit.transform.GetComponent < Rigidbody > () ) { //When 'raycastHit.collider.transform' is Turret, 'raycastHit.transform' is the MainBody. // Debug.Log("ray hit the object " + raycastHit.transform.gameObject.name); targetTransform = colliderTransform ; targetOffset = targetTransform.InverseTransformPoint ( raycastHit.point ) ; return ; } else { targetTransform = null ; } } targetPos = raycastHit.point ; } else { // Ray hits itsel // Debug.Log("hit itself!!!"); screenPos.z = 50.0f ; targetPos = Camera.main.ScreenToWorldPoint ( screenPos ) ; } } else { // Ray does not hit anythig. // Debug.Log("no hit!!"); screenPos.z = 500.0f ; targetPos = Camera.main.ScreenToWorldPoint ( screenPos ) ; } } */ #if UNITY_ANDROID || UNITY_IPHONE void Cast_Ray_Mobile ( Vector3 screenPos ) { Ray ray = Camera.main.ScreenPointToRay ( screenPos ) ; RaycastHit [] raycastHits ; raycastHits = Physics.SphereCastAll ( ray , spherecastRasius , 1000.0f ) ; foreach ( RaycastHit raycastHit in raycastHits ) { Transform colliderTransform = raycastHit.collider.transform ; if ( colliderTransform.root != rootTransform ) { if ( raycastHit.transform.GetComponent < Rigidbody > () ) { //When 'raycastHit.collider.transform' is Turret, 'raycastHit.transform' is the MainBody. targetTransform = colliderTransform ; targetOffset = targetTransform.InverseTransformPoint ( raycastHit.point ) ; return ; } } } Cast_Ray ( screenPos , true ) ; } #endif void FixedUpdate () { // Calculate Angle. if(AI_Wheel_Control_Script.targetAITransform != null) { targetPos = AI_Wheel_Control_Script.targetAITransform.position; } local_TargetPos = thisTransform.InverseTransformPoint ( targetPos ) ; // Debug.Log ("local position is: " + local_TargetPos); float targetAng = Vector2.Angle ( Vector2.up , new Vector2 ( local_TargetPos.x , local_TargetPos.z ) ) * Mathf.Sign ( local_TargetPos.x ) ; //angle if(targetAng < 0.5f && targetAng > -0.5f) { //the turret have target at the enemy! if(AI_Wheel_Control_Script.closeEnoughToShoot) BroadcastMessage ("PointToTargetAndCloseEnough", SendMessageOptions.DontRequireReceiver); } // Debug.Log ("relatived target is: " + targetAng); // Calculate Turn Rate. float targetSpeedRate = Mathf.Lerp ( 0.0f , 1.0f , Mathf.Abs ( targetAng ) / ( rotationSpeed * Time.fixedDeltaTime + bufferAngle ) ) * Mathf.Sign ( targetAng ) ; // Calculate Rate speedRate = Mathf.MoveTowardsAngle ( speedRate , targetSpeedRate , Time.fixedDeltaTime / acceleration_Time ) ; // Rotate currentAng += rotationSpeed * speedRate * Time.fixedDeltaTime ; thisTransform.localRotation = Quaternion.Euler ( new Vector3 ( 0.0f , currentAng , 0.0f ) ) ; } void Get_ID ( int idNum ) { myID = idNum ; } void Get_Current_ID ( int idNum ) { if ( idNum == myID ) { isCurrentID = true ; } else { isCurrentID = false ; } } }
AI_Cannon_Control.cs
using UnityEngine; using System.Collections; // This script must be attached to "Cannon_Base". public class AI_Cannon_Control : MonoBehaviour { [ Header ( "Cannon movement settings" ) ] [ Tooltip ( "Rotation Speed. (Degree per Second)" ) ] public float rotationSpeed = 5.0f ; [ Tooltip ( "Angle range for slowing down. (Degree)" ) ] public float bufferAngle = 1.0f ; [ Tooltip ( "Maximum elevation angle. (Degree)" ) ] public float maxElev = 3f ; [ Tooltip ( "Maximum depression angle. (Degree)" ) ] public float maxDep = 0.5f ; Transform thisTransform ; AI_Turret_Control turretScript ; float currentAng ; float adjustAng; bool up; void Start () { thisTransform = this.transform ; up = true; turretScript = thisTransform.GetComponentInParent < AI_Turret_Control > () ; if ( turretScript == null ) { Debug.LogError ( "Cannon_Base cannot find Turret_Control_CS." ) ; Destroy ( this ) ; } currentAng = thisTransform.localEulerAngles.x ; } void FixedUpdate () { // Calculate Angle. if (up) { //positive adjustAng = Random.Range (0, 300); } else { //negative adjustAng = (-1) * Random.Range (0, 300); } float targetAng = Mathf.Rad2Deg * ( Mathf.Asin ( ( turretScript.local_TargetPos.y - thisTransform.localPosition.y ) / Vector3.Distance ( thisTransform.localPosition , turretScript.local_TargetPos ) ) ) ; targetAng += Mathf.DeltaAngle ( 0.0f , thisTransform.localEulerAngles.x ) + adjustAng ; // targetAng += Mathf.DeltaAngle ( 0.0f , thisTransform.localEulerAngles.x ); // Calculate Speed Rate float speedRate = -Mathf.Lerp ( 0.0f , 1.0f , Mathf.Abs ( targetAng ) / ( rotationSpeed * Time.fixedDeltaTime + bufferAngle ) ) * Mathf.Sign ( targetAng ) ; // Rotate currentAng += rotationSpeed * speedRate * Time.fixedDeltaTime ; currentAng = Mathf.Clamp ( currentAng , -maxElev , maxDep ) ; thisTransform.localRotation = Quaternion.Euler ( new Vector3 ( currentAng , 0.0f , 0.0f ) ) ; if ((maxDep - currentAng) <= 0.01) up = true; if ((currentAng + maxElev) <= 0.01) up = false; } }
AI_Fire_Control.cs
using UnityEngine; using System.Collections; using UnityStandardAssets.CrossPlatformInput; // This script must be attached to "Cannon_Base". public class AI_Fire_Control : MonoBehaviour { [ Header ( "Fire control settings" ) ] [ Tooltip ( "Loading time. (Sec)" ) ] public float reloadTime = 4.0f ; [ Tooltip ( "Recoil force with firing." ) ] public float recoilForce = 5000.0f ; bool isReady = true ; bool pointToTarget = false; Transform thisTransform ; Rigidbody parentRigidbody ; int myID ; bool isCurrentID = true ; void Start () { thisTransform = this.transform ; parentRigidbody = GetComponentInParent < Rigidbody > () ; if ( parentRigidbody == null ) { Debug.LogError ( "Rigidbody is not found in MainBody." ) ; Destroy ( this ) ; } } void Update () { if ( isCurrentID ) { #if UNITY_ANDROID || UNITY_IPHONE if ( CrossPlatformInputManager.GetButtonUp ( "GunCam_Press" ) && isReady ) { #else if ( pointToTarget && isReady ) { #endif // Send message to this and 'Barrel_Control' and 'Fire_Spawn'. BroadcastMessage ( "Fire" , SendMessageOptions.DontRequireReceiver ) ; } } } void PointToTargetAndCloseEnough() { pointToTarget = true; } void Fire () { parentRigidbody.AddForceAtPosition ( -thisTransform.forward * recoilForce , thisTransform.position , ForceMode.Impulse ) ; pointToTarget = false; isReady = false ; StartCoroutine ( "Reload" ) ; } IEnumerator Reload () { yield return new WaitForSeconds ( reloadTime ) ; isReady = true ; } void Get_ID ( int idNum ) { myID = idNum ; } void Get_Current_ID ( int idNum ) { if ( idNum == myID ) { isCurrentID = true ; } else { isCurrentID = false ; } } }
3.注意:
这些脚本是我自己改写的,AI坦克核心的控制脚本。如果只看这些脚本,可能还是无法理解坦克究竟是怎么运动的,因为里面还要用到注入Wheel_Sysn等脚本。可以直接下载我的project,看一下AI坦克的构成部件,以及上面挂着的脚本。
5.一点心得:
1.这次主要目标是实现AI坦克,构建一个预制件。游戏是其次,加上最近时间比较紧,所以没有用到诸如工厂模式,导演和SceneController这一些东西,以后有时间再补上。
2.这次的AI坦克还是比较naive的。
3.学到了不少东西,嘿嘿。以后看看能不能结合navMesh再做一个。