3D Game Programming & Design:遊戲智能
遊戲智能概述
遊戲智能是指:
在遊戲規則約束下,通過適當的 算法 使得遊戲中 NPC(Non-Player Character) 呈現爲具有一定人類智能行爲的 博弈對手,讓遊戲玩家面臨不間斷的挑戰,並在挑戰中有所收穫,包括知識和技能等。
遊戲智能需要具備以下幾個性質:
- 擬人化。遊戲智能更注重遊戲對象行爲結果類似(模擬)人,既能做出令人驚訝的有效行爲,也會犯各種愚笨的錯誤,從而與現實世界中不同的人匹配。而人工智能通常是超人設計,追求最佳結果。
- 可玩性。遊戲智能並不意味着高大上的算法,它更注重針對不同類型的玩家設計不同能力的 NPC,例如:“小怪物”,“大boss”等等,通常用等級這個參數來表達遊戲智能 agent 的能力。
- 趣味性。遊戲智能設計注重娛樂,而不只是算法研究。因此,算法會集合一些特效,讓人感到愉悅。
模型、方法與常用算法
“感知-思考-行爲”模型
“sense-think-act” paradigm(範式) 是構造 agent、robot、NPC(Non-Player Character) 的基礎概念。自從上世紀80年代提出以來,我們使用 Sense-Think-Act 範例思考機器人如何工作,並設計它們。
1、感知(Sense)
感知是 agent 接收世界信息的行爲,其獲取的數據將是思考的輸入。這裏主要要考慮的問題是如何限制信息獲取是設計不同級別 agent 的核心問題。
在遊戲中,定義獲取信息能力通常可以從視覺、聽覺和嗅覺等渠道去考慮:
-
視覺(Vision)
識別“敵人”的位置和屬性 識別“障礙物”及其範圍
-
聽覺(Sound)
識別事件的方向和距離
-
嗅覺(Smell)
獲得玩家/事件的痕跡
使用距離、角度、障礙物限制 agent 發現玩家位置等信息,或者使用干擾影響信息的準確性,這些是設計遊戲 agent 常見的手段。如果 agent 都擁有玩家位置和導航地圖,結果就是一窩蜂的現象,這是沒有趣的事情。合適的限制,將使得 agent 在相同的決策算法下,呈現豐富的行爲。
一種簡單的方法就是 agent 結合不同類型的觸發器、探頭,構造不同的獲取信息的場景。最後將感知的結果放在一個數據結構中(類似 UnityEngine.EventSystems.PointerEventData)
在 Agent Thinking 時,不可獲取感知以外的數據信息。
2、思考(Think)
Think 就是算法,它的輸入是感知的數據,輸出是行爲(behaviours)。 思考的算法,通常就是我們所說的遊戲規則的一部分,即 agent 能做什麼,該做什麼。 遊戲 agent 的思考類似人腦的決策過程,建立符合遊戲玩家難度曲線,可以控制、且符合社會準則的行爲。另一個相關問題,玩家難度曲線在編程階段是未知的,依賴衆多玩家與 agent 的操作與對抗結果。它在遊戲測試和運維過程中存在巨大不確定性。因此將 agent 決策過程用 If … Then … 硬編碼寫入程序邏輯是不可行的!
目前主要的方法有:
規則推理引擎
規則推理引擎(Rule-based Inference Engine)也叫產生式系統或推理模型(inference model),是由基於規則的專家系統發展而來。基於規則的專家系統,基本概念包括:
- 事實(facts): 事實是用來表示已知的數據或信息。
- 規則(Rules): 即產生式規則,用來表示系統推理的有關知識。
規則由條件和動作組成,格式一般爲:IF 條件 Then 動作,例如 Rule1: Human(x) => Mortal(x)
(一階謂詞邏輯,離散數學)
狀態機引擎
有限狀態自動機(Finite State Machine / FSM)是可以圖形化的自動執行工具。在 unity 中,它是標準化的 agent(NPC) 動作自動控制工具。定義一個狀態機主要工作包括:
- 狀態:該組件定義了一組狀態,一個遊戲實體或NPC可以選擇(巡邏、追逐和射擊)
- 轉移:該組件定義了不同狀態之間的關係
- 規則:該組件用來觸發狀態轉移(玩家在視線範圍內、距離足夠攻擊、丟失/殺死玩家)
- 事件:該組件用於觸發檢查規則(守衛的可見區域、與玩家的距離等)
決策樹
決策樹(Decision Trees)又稱爲行爲樹(Behaviour Trees)。
3、行動(Act)
行動(Act)將思考(Think)的結果作爲輸入,該部分的任務就是使得 agent 行爲更符合物理世界的規律,使得“心想事成”這樣理想的結果變得不確定。
通常需要考慮的要素包括:
- 準備時間。在準備時間通過光、聲等方式提示對手
- 動作時間。從動作開始到結束是一個序列
- 干擾因素。利用風、地形、隨機數使炮彈有一定偏差
Unity 3D 導航與尋路
Unity 導航系統允許創建給遊戲角色導航的遊戲世界。遊戲角色可以在藍色聯通的網格上,找到去任意一點最短的路徑,且具有一定爬坡、跳溝壑的能力。
- NavMesh (Navigation Mesh) 是一種數據結構,它描述了遊戲對象可行走的表面。通過三角網格,計算其中任意兩點之間的最短路徑,用於遊戲對象的導航。它是根據場景幾何結構自動創建或烘焙構建。
- NavMesh Agent組件創建具有尋路能力的角色。Agent 使用NavMesh 推理,避免彼此以及移動障礙物。
- Off-Mesh Link組件允許將不連接的塊之間建立“傳送門”。例如,跳過溝渠或圍欄,或在穿過它之前打開門,都可以被描述爲 Off-Mesh Link。
- NavMesh 障礙 組件允許您描述 agent 在移動時應避免的移動障礙。由物理系統控制的桶或箱子就是很典型的障礙。在障礙物移動的過程中,Agent 盡力避開它,但一旦障礙物變得靜止,它將在導航網格上開一個洞,以便 Agent 可以改變他們的路徑以繞過它,或者靜止的障礙物阻塞路徑,使得 Agent 找到其他路線。
編程實踐
作業要求
本次作業基本要求是三選一,我選擇的是第二個:坦克對戰遊戲 AI 設計。
具體如下:
從商店下載遊戲:“Kawaii” Tank 或 其他坦克模型,構建 AI 對戰坦克。具體要求
- 使用“感知-思考-行爲”模型,建模 AI 坦克
- 場景中要放置一些障礙阻擋對手視線
- 坦克需要放置一個矩陣包圍盒觸發器,以保證 AI 坦克能使用射線探測對手方位
- AI 坦克必須在有目標條件下使用導航,並能繞過障礙。(失去目標時策略自己思考)
- 實現人機對戰
本次作業參考和使用了Unity3D官方教程的Tanks! Tutorial裏面的模型進行構建。同時參考了博客Unity 3D官方教程——Tanks!學習記錄來對各項參數進行配置。
代碼分析
添加文本實現坦克的各種運動,打開Scripts下Tank下TankMovement腳本並編輯。這裏實現了血量的初始化和控制坦克在被子彈擊中後的血量變化以及發射子彈的操作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Tank : MonoBehaviour {
private float hp =1000.0f;
// 初始化
public Tank()
{
hp = 1000.0f;
}
public float getHP()
{
return hp;
}
public void setHP(float hp)
{
this.hp = hp;
}
public void beShooted()
{
hp -= 100;
}
public void shoot(TankType type)
{
GameObject bullet = Singleton<MyFactory>.Instance.getBullets(type);
bullet.transform.position = new Vector3(transform.position.x, 2.0f, transform.position.z) + transform.forward * 2.0f;
bullet.transform.forward = transform.forward; //方向
bullet.GetComponent<Rigidbody>().AddForce(bullet.transform.forward * 20, ForceMode.Impulse);
}
}
AI坦克也就是敵方坦克是我們本次實驗的一個重點,這裏我們就是利用的前面幾個部分所講的原則和技術來實現使NPC自動巡航定位玩家的位置並且自動向玩家位置靠近。
導航利用的是NavMeshAgent agent組件創建具有尋路能力的AI坦克。Agent 使用NavMesh 推理,能夠避免彼此碰撞以及移動障礙物等操作。此外,我們利用協程來實現自動射擊,當距離玩家距離在10以內時,就會發射子彈。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class Enemy : Tank {
public delegate void RecycleEnemy(GameObject enemy);
//當enemy被摧毀時,通知工廠回收;
public static event RecycleEnemy recycleEnemy;
// player 的位置
private Vector3 playerLocation;
private bool gameover;
private void Start()
{
playerLocation = GameDirector.getInstance().currentSceneController.getPlayer().transform.position;
StartCoroutine(shoot());
}
void Update() {
playerLocation = GameDirector.getInstance().currentSceneController.getPlayer().transform.position;
gameover = GameDirector.getInstance().currentSceneController.getGameOver();
if (!gameover)
{
if (getHP() <= 0 && recycleEnemy != null)
{
recycleEnemy(this.gameObject);
}
else
{
// 自動向player移動
NavMeshAgent agent = gameObject.GetComponent<NavMeshAgent>();
agent.SetDestination(playerLocation);
}
}
else
{
//遊戲結束,停止尋路
NavMeshAgent agent = gameObject.GetComponent<NavMeshAgent>();
agent.velocity = Vector3.zero;
agent.ResetPath();
}
}
// 協程實現每隔1s進行射擊,開始喜歡協程了
IEnumerator shoot()
{
while (!gameover)
{
for(float i =1;i> 0; i -= Time.deltaTime)
{
yield return 0;
}
if(Vector3.Distance(playerLocation,gameObject.transform.position) < 10)
{
shoot(TankType.ENEMY);
}
}
}
}
玩家坦克是對坦克類的繼承,因爲玩家是需要我們控制它的射擊,方向,以及移動的,所以這裏主要是在實現這些功能。
控制前進後退的分別是W/S,控制左右方向旋轉的是A/D
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : Tank{
public delegate void DestroyPlayer(); // game over!
public static event DestroyPlayer destroyEvent;
void Start () {
setHP(1000);
}
// Update is called once per frame
void Update () {
if(getHP() <= 0) // game over!
{
this.gameObject.SetActive(false);
destroyEvent();
}
}
//鍵盤w,s控制前後移動
public void moveForward()
{
gameObject.GetComponent<Rigidbody>().velocity = gameObject.transform.forward * 50;
}
public void moveBackWard()
{
gameObject.GetComponent<Rigidbody>().velocity = gameObject.transform.forward * -50;
}
//鍵盤a,d控制原地左右旋轉的方向。通過水平軸上的增量,改變玩家坦克的歐拉角,從而實現坦克轉向
public void turn(float offsetX)
{
float x = gameObject.transform.localEulerAngles.x;
float y = gameObject.transform.localEulerAngles.y + offsetX*2;
gameObject.transform.localEulerAngles = new Vector3(x, y, 0);
}
}
子彈類首先是要區分出攻擊的是敵方還是己方,所以我們先對坦克類型做一個判斷,如果是敵方纔會攻擊,否則就不會射擊。
同時爲了讓遊戲的可玩性更強,我們設置了玩家的攻擊力度要比AI坦克更強,降低了遊戲難度。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour {
public float explosionRadius = 3.0f;
private TankType tankType;
//設置發射子彈的坦克類型
public void setTankType(TankType type)
{
tankType = type;
}
private void OnCollisionEnter(Collision collision)
{
if(collision.transform.gameObject.tag == "tankEnemy" && this.tankType == TankType.ENEMY ||
collision.transform.gameObject.tag == "tankPlayer" && this.tankType == TankType.PLAYER)
{
return;
}
MyFactory factory = Singleton<MyFactory>.Instance;
ParticleSystem explosion = factory.getParticleSystem();
explosion.transform.position = gameObject.transform.position;
//獲取爆炸範圍內的所有碰撞體
Collider[] colliders = Physics.OverlapSphere(gameObject.transform.position, explosionRadius);
foreach(var collider in colliders)
{
//被擊中坦克與爆炸中心的距離
float distance = Vector3.Distance(collider.transform.position, gameObject.transform.position);
float hurt;
// 如果是玩家發出的子彈傷害高一點
if (collider.tag == "tankEnemy" && this.tankType == TankType.PLAYER)
{
hurt = 300.0f / distance;
collider.GetComponent<Tank>().setHP(collider.GetComponent<Tank>().getHP() - hurt);
}
else if(collider.tag == "tankPlayer" && this.tankType == TankType.ENEMY)
{
hurt = 100.0f / distance;
collider.GetComponent<Tank>().setHP(collider.GetComponent<Tank>().getHP() - hurt);
}
explosion.Play();
}
if (gameObject.activeSelf)
{
factory.recycleBullet(gameObject);
}
}
}
然後工廠類就是生產和回收上述角色,這個在前面我們經常用到。場景類就是一個實例化的過程,進行初始化和實現UI類的操作。UI類主要是和用戶的交互,檢測玩家輸入和輸出遊戲結果
等。因爲在之前的作業中這些都已經涉及到過了,所以這裏就不再代碼分析啦,詳情看GitHub上的代碼吧。
遊戲界面效果