AI坦克大战
实验内容
坦克对战游戏 AI 设计
- 从商店下载游戏:“Kawaii” Tank 或 其他坦克模型,构建 AI 对战坦克。具体要求
- 使用“感知-思考-行为”模型,建模 AI 坦克
- 场景中要放置一些障碍阻挡对手视线
- 坦克需要放置一个矩阵包围盒触发器,以保证 AI 坦克能使用射线探测对手方位
- AI 坦克必须在有目标条件下使用导航,并能绕过障碍。(失去目标时策略自己思考)
- 实验人机对战
实验结果
- 实验的视频链接 :https://pan.baidu.com/s/1qs5FSylANnUW7lnCiyrnRA
- 实验的代码下载 :https://download.csdn.net/download/qq_36312878/10486779
实验操作
实现的思路
个人觉得本次的实验,重点在于对上课时的“感知-思考-行为”模型的理解,同时还在于对老师要求的下载的预制的代码阅读(代码量有点多,需要花费大量时间来理解这个预制是如何进行工作的)
实现AI的思路,在于坦克能够根据目前的情况来进行相对应的判断,以下是我的设计思路:
- 但视野内没有玩家,坦克会不停旋转,进行360度无死角的扫描
- 但视野内出现玩家时,首先对玩家的位置进行判断,进行左转或者右转操作,并判断两者之间的距离,如果距离超过100,则进行前进;当距离小于50时,会进行开火
预制的代码解析:
以下是预制中部分需要用到的脚本文件的作用:
- ID_Control_CS:是整个坦克的控制中心
- Wheel_Control_CS:坦克移动的输入,不过只进行前进后退操作
- Wheel_Rotate_CS:控制坦克的转动
- Fire_Control_CS:控制坦克的开火
其他代码其实可以做进一步的修改,达到自己想要的AI效果
细节的实现
视野的扫描(感知)
通过发射射线来进行检查,射线撞到的第一个物体是否是玩家(可以添加标签来确定);
同时,我的视野是左边45度,到右边45度,这里可以通过调整射线的发射方向来实现,这个用矢量相加即可。我是用等差控制,不过这个有个弊端,离得远的话,可能有些在视野内扫描不到。但是这也符合了显示生活,远距离可能会看不清或者忽略。
操作的判断(思考)
- 首先是判断是否找到玩家,没有则原地打转
- 再判断彼此间的位置关系,如果在左边则左转,右边则右转。这里用到了函数InverseTransformPoint,返回一个向量代表两者间的关系
- 再对坦克跟目标点之间距离的判断,如果超过100,则前进,如果小于50则开火射击
AI行动(行动)
通过添加一系列的参数,来代替键盘跟鼠标的输入,从而来调动坦克的行为。这里坦克如何行动不用我们去实现,我只需要模拟要的行为的相关按键即可实现。
用到参数如下:
- fireButton:判断是否进行开火
- leftButton:判断是否进行左转
- rightButton:判断是否进行右转
- upButton:判断是否进行前进
代码的具体实现:
主要是修改了ID_Control_CS跟Wheel_Control_CS、Fire_Control_CS三个文件的内容:
ID_Control_CS的代码:
using UnityEngine;
using System;
using System.Collections;
using UnityEngine.UI;
// This script must be attached to the top object of the tank.
namespace ChobiAssets.KTP
{
public class ID_Control_CS : MonoBehaviour
{
[Header ("ID settings")]
[Tooltip ("ID number")] public int id = 0;
[HideInInspector] public bool isPlayer; // Referred to from child objects.
[HideInInspector] public Game_Controller_CS controllerScript;
[HideInInspector] public TankProp storedTankProp; // Set by "Game_Controller_CS".
[HideInInspector] public Turret_Control_CS turretScript;
[HideInInspector] public Camera_Zoom_CS mainCamScript;
[HideInInspector] public GunCamera_Control_CS gunCamScript;
void Start ()
{ // Do not change to "Awake ()".
// Send this reference to the "Game_Controller" in the scene.
GameObject gameController = GameObject.FindGameObjectWithTag ("GameController");
if (gameController) {
controllerScript = gameController.GetComponent <Game_Controller_CS> ();
}
if (controllerScript) {
controllerScript.Receive_ID (this);
} else {
Debug.LogError ("There is no 'Game_Controller' in the scene.");
}
// Broadcast this reference.
BroadcastMessage ("Get_ID_Script", this, SendMessageOptions.DontRequireReceiver);
}
#if !UNITY_ANDROID && !UNITY_IPHONE
[HideInInspector] public bool aimButton;
[HideInInspector] public bool aimButtonDown;
[HideInInspector] public bool aimButtonUp;
[HideInInspector] public bool dragButton;
[HideInInspector] public bool dragButtonDown;
[HideInInspector] public bool fireButton;//判断是否开火
[HideInInspector] public bool leftButton = false;//判断是否向左
[HideInInspector] public bool rightButton = false;//判断是否向右
[HideInInspector] public bool upButton = false;//判断是否前进
void Update ()
{
if (isPlayer) {
aimButton = Input.GetKey (KeyCode.Space);
aimButtonDown = Input.GetKeyDown (KeyCode.Space);
aimButtonUp = Input.GetKeyUp (KeyCode.Space);
dragButton = Input.GetMouseButton (1);
dragButtonDown = Input.GetMouseButtonDown (1);
fireButton = Input.GetMouseButton (0);
}
else
{
}
}
#endif
void Destroy ()
{ // Called from "Damage_Control_CS".
gameObject.tag = "Finish";
}
public void Get_Current_ID (int currentID)
{ // Called from "Game_Controller_CS".
if (id == currentID) {
isPlayer = true;
} else {
isPlayer = false;
}
// Call Switch_Player.
turretScript.Switch_Player (isPlayer);
mainCamScript.Switch_Player (isPlayer);
gunCamScript.Switch_Player (isPlayer);
}
}
}
Wheel_Control_CS的代码:
using UnityEngine;
using System.Collections;
#if UNITY_ANDROID || UNITY_IPHONE
using UnityStandardAssets.CrossPlatformInput;
#endif
// This script must be attached to "MainBody".
namespace ChobiAssets.KTP
{
public class Wheel_Control_CS : MonoBehaviour
{
[ Header ("Driving settings")]
[ Tooltip ("Torque added to each wheel.")] public float wheelTorque = 3000.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, 2.0f)] public float turnClamp = 0.8f;
[ Tooltip ("'Solver Iteration Count' of all the rigidbodies in this tank.")] public int solverIterationCount = 7;
// Reference to "Wheel_Rotate".
[HideInInspector] public float leftRate;
[HideInInspector] public float rightRate;
Rigidbody thisRigidbody;
bool isParkingBrake = false;
float lagCount;
float speedStep;
float autoParkingBrakeVelocity = 0.5f;
float autoParkingBrakeLag = 0.5f;
ID_Control_CS idScript;
/* for reducing Calls.
Wheel_Rotate_CS[] rotateScripts;
*/
void Awake ()
{
this.gameObject.layer = 11; // Layer11 >> for MainBody.
thisRigidbody = GetComponent < Rigidbody > ();
thisRigidbody.solverIterations = solverIterationCount;
/* for reducing Calls.
rotateScripts = GetComponentsInChildren <Wheel_Rotate_CS> ();
*/
}
void Update ()
{
if (idScript.isPlayer) {
#if UNITY_ANDROID || UNITY_IPHONE
Mobile_Input ();
#else
Desktop_Input ();
#endif
}
else
{
RaycastHit hit;
Vector3 target = new Vector3(0,0,0);
float i = 1.0f;
bool flag = false;
//当检测到时,需要break去进行操作
while(i >= -1.0f)
{
Vector3 _dir = new Vector3(1, 0, 0);
_dir.z += i;
i -= 0.01f;
if (Physics.Raycast(transform.position, _dir, out hit))
{
if (hit.rigidbody != null)
{
//Debug.Log(hit.rigidbody.gameObject.tag);
//Debug.Log(hit.rigidbody.gameObject.transform.position);
if (hit.rigidbody.gameObject.tag == "Player")
{
flag = true;
target = hit.rigidbody.gameObject.transform.position;
break;
}
}
}
}
if(flag)
{
Vector3 localPositionOfTarget = this.transform.InverseTransformPoint(target);
//确定好运动的方向
if(localPositionOfTarget.x > 0)
{
idScript.rightButton = true;
idScript.leftButton = false;
}
else if(localPositionOfTarget.x < 0)
{
idScript.rightButton = false;
idScript.leftButton = true;
}
else
{
idScript.rightButton = false;
idScript.leftButton = false;
}
//确定是否前进与开火
var dis = Vector3.Distance(target, this.transform.position);
if(dis > 50)
{
idScript.upButton = true;
idScript.fireButton = false;
}
else
{
idScript.upButton = false;
idScript.fireButton = true;
}
}
else
{
idScript.fireButton = false;
idScript.leftButton = true;
idScript.rightButton = false;
idScript.upButton = false;
}
AI_Input();
}
}
#if UNITY_ANDROID || UNITY_IPHONE
void Mobile_Input ()
{
if (CrossPlatformInputManager.GetButtonDown ("Up")) {
speedStep += 0.5f;
speedStep = Mathf.Clamp (speedStep, -1.0f, 1.0f);
} else if (CrossPlatformInputManager.GetButtonDown ("Down")) {
speedStep -= 0.5f;
speedStep = Mathf.Clamp (speedStep, -1.0f, 1.0f);
}
float vertical = speedStep;
float horizontal = 0.0f;
if (CrossPlatformInputManager.GetButton ("Left")) {
horizontal = Mathf.Lerp (-turnClamp, -1.0f, Mathf.Abs (vertical / 1.0f));
} else if (CrossPlatformInputManager.GetButton ("Right")) {
horizontal = Mathf.Lerp (turnClamp, 1.0f, Mathf.Abs (vertical / 1.0f));
}
if (vertical < 0.0f) {
horizontal = -horizontal; // like a brake-turn.
}
leftRate = Mathf.Clamp (-vertical - horizontal, -1.0f, 1.0f);
rightRate = Mathf.Clamp (vertical - horizontal, -1.0f, 1.0f);
}
#else
void AI_Input()
{
if (idScript.upButton)
{
speedStep += 0.5f;
speedStep = Mathf.Clamp(speedStep, -1.0f, 1.0f);
}
else if (Input.GetKeyDown(KeyCode.DownArrow) || Input.GetKeyDown(KeyCode.S))
{
speedStep -= 0.5f;
speedStep = Mathf.Clamp(speedStep, -1.0f, 1.0f);
}
else if (Input.GetKeyDown(KeyCode.X))
{
speedStep = 0.0f;
}
float vertical = speedStep;
//float horizontal = Input.GetAxis("Horizontal");
float horizontal = 0;
if (idScript.leftButton) horizontal = -0.05f;
if (idScript.rightButton) horizontal = 0.05f;
float clamp = Mathf.Lerp(turnClamp, 1.0f, Mathf.Abs(vertical / 1.0f));
horizontal = Mathf.Clamp(horizontal, -clamp, clamp);
if (vertical < 0.0f)
{
horizontal = -horizontal; // like a brake-turn.
}
leftRate = Mathf.Clamp(-vertical - horizontal, -1.0f, 1.0f);
rightRate = Mathf.Clamp(vertical - horizontal, -1.0f, 1.0f);
}
void Desktop_Input ()
{
if (Input.GetKeyDown (KeyCode.UpArrow) || Input.GetKeyDown (KeyCode.W)) {
speedStep += 0.5f;
speedStep = Mathf.Clamp (speedStep, -1.0f, 1.0f);
} else if (Input.GetKeyDown (KeyCode.DownArrow) || Input.GetKeyDown (KeyCode.S)) {
speedStep -= 0.5f;
speedStep = Mathf.Clamp (speedStep, -1.0f, 1.0f);
} else if (Input.GetKeyDown (KeyCode.X)) {
speedStep = 0.0f;
}
float vertical = speedStep;
float horizontal = Input.GetAxis ("Horizontal");
//Debug.Log(horizontal);
float clamp = Mathf.Lerp (turnClamp, 1.0f, Mathf.Abs (vertical / 1.0f));
horizontal = Mathf.Clamp (horizontal, -clamp, clamp);
if (vertical < 0.0f) {
horizontal = -horizontal; // like a brake-turn.
}
leftRate = Mathf.Clamp (-vertical - horizontal, -1.0f, 1.0f);
rightRate = Mathf.Clamp (vertical - horizontal, -1.0f, 1.0f);
}
#endif
void FixedUpdate ()
{
// Auto Parking Brake using 'RigidbodyConstraints'.
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;
}
/* for reducing Calls.
for (int i = 0; i < rotateScripts.Length; i++) {
rotateScripts [i].FixedUpdate_Me ();
}
*/
}
void Destroy ()
{ // Called from "Damage_Control_CS".
StartCoroutine ("Disable_Constraints");
}
IEnumerator Disable_Constraints ()
{
// Disable constraints of MainBody's rigidbody.
yield return new WaitForFixedUpdate (); // This wait is required for PhysX.
thisRigidbody.constraints = RigidbodyConstraints.None;
Destroy (this);
}
void Get_ID_Script (ID_Control_CS tempScript)
{
idScript = tempScript;
}
void Pause (bool isPaused)
{ // Called from "Game_Controller_CS".
this.enabled = !isPaused;
}
}
}
Fire_Control_CS的代码:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
#if UNITY_ANDROID || UNITY_IPHONE
using UnityStandardAssets.CrossPlatformInput;
#endif
// This script must be attached to "Cannon_Base".
namespace ChobiAssets.KTP
{
public class Fire_Control_CS : 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;
Transform thisTransform;
Rigidbody bodyRigidbody;
ID_Control_CS idScript;
Barrel_Control_CS[] barrelScripts;
Fire_Spawn_CS[] fireScripts;
void Awake ()
{
thisTransform = this.transform;
barrelScripts = GetComponentsInChildren <Barrel_Control_CS> ();
fireScripts = GetComponentsInChildren <Fire_Spawn_CS> ();
}
void Update ()
{
if (idScript.isPlayer) {
#if UNITY_ANDROID || UNITY_IPHONE
Mobile_Input ();
#else
Desktop_Input ();
#endif
}
else
{
if(isReady && idScript.fireButton) Fire();
}
}
#if UNITY_ANDROID || UNITY_IPHONE
void Mobile_Input ()
{
if (CrossPlatformInputManager.GetButtonUp ("GunCam_Press") && isReady) {
Fire ();
}
}
#else
void Desktop_Input ()
{
if (idScript.fireButton && isReady) {
Fire ();
}
}
#endif
void Fire ()
{
// Call barrelScripts and fireScripts to fire.
for (int i = 0; i < barrelScripts.Length; i++) {
barrelScripts [i].Fire ();
}
for (int i = 0; i < fireScripts.Length; i++) {
fireScripts [i].StartCoroutine ("Fire");
}
// Add recoil shock.
bodyRigidbody.AddForceAtPosition (-thisTransform.forward * recoilForce, thisTransform.position, ForceMode.Impulse);
isReady = false;
StartCoroutine ("Reload");
}
IEnumerator Reload ()
{
yield return new WaitForSeconds (reloadTime);
isReady = true;
}
void Destroy ()
{ // Called from "Damage_Control_CS".
Destroy (this);
}
void Get_ID_Script (ID_Control_CS tempScript)
{
idScript = tempScript;
bodyRigidbody = idScript.storedTankProp.bodyRigidbody;
}
void Pause (bool isPaused)
{ // Called from "Game_Controller_CS".
this.enabled = !isPaused;
}
}
}
实验反思:
- 由于预制是有提供场景的,因此并未用到课堂上学习到的导航
- 另外,同样跟师兄博客的内容一样,给坦克加了刚体后,出现了莫名奇妙的鬼畜,应该是刚体本身跟地板的碰撞,因为坦克本身在颤抖
- 其实感觉AI还是可以进一步来优化,不过时间比较紧,就没做得太完善了,暑假有时间再进一步的修改