【Unity3D開發小遊戲】《戰棋小遊戲》Unity開發教程

恬靜的小魔龍發表於2020-03-09

一、前言

這次想要做的一個小遊戲,或者說一個小Demo,其實是一個簡單且傳統的戰棋戰鬥場景。初步的設計是:在2D世界裡建立一張由六邊形地塊組成的戰鬥地圖,敵我雙方依據體力在地圖上輪流行動並向對方發動攻擊,先消滅掉所有敵人的一方將獲得勝利。

這一輯將比上一輯的內容更簡單,但完成後會是一個功能較完整且可以玩耍的Demo。

我使用的Unity版本是2018.2.7,但是其實並沒有用到2018的任何新功能。

二、Github地址

Github:github.com/elsong823/H… *可以自己下載原始碼

三、正文


預計將分為以下幾篇:

1、建立戰場 根據預定尺寸生成戰場地圖,並隨機一些障礙物。

2、新增對戰雙方 向戰場中新增作戰單位,作戰單位可被點選,並進行移動。

3、戰場邏輯 作戰雙方按照順序依次行動,可進行移動、攻擊。

4、新增戰場UI 新增可以隨時顯示戰況的Hud、為作戰單位新增血條等。

5、擴充套件作戰單位 豐富作戰單位的型別,新增職業,並加入若干不同型別的技能。

6、擴充套件戰場地圖 豐富戰場地圖,加入地形及道具等元素。

7、規範戰鬥配置 可以通過規範化的資料結構配置戰場、職業、技能、道具等。


一、建立戰場

| 目標 生成一個規定尺寸的戰場,戰場上的格子均為六邊形,並且暫定兩種格子型別:普通格子和障礙格子。

| 在開始之前我做了以下事情 1.建立了新的工程。 2.新建場景並儲存在場景資料夾下。 3.刪除預設建立的平行光,調整場景光照設定,棄用了天空盒等。 4.將相機調整為正交相機。 因為是從零點五開始系列,認為讀者有一定的Unity引擎使用基礎,就不對這些操作進行介紹了。

| 建立戰場地圖 我沒有采用把資料和顯示捆綁在一起的做法,而是將資料和顯示分離。 我的思路是用一個戰鬥建立器來建立戰鬥,一場完整的戰鬥資訊至少應包含地圖及對戰雙方的資訊。而Unity搭建的戰鬥場景則可以理解為一場戰鬥的顯示器。

| 資料部分(主要) 戰鬥資料:地圖尺寸,包含所有格子的二維陣列。 格子資料:格子型別,所在行列,所在空間座標。

在這裡插入圖片描述
| 顯示部分(主要) 戰場:一個用於組織所有格子物件的節點物件。 格子物件:一個能顯示六邊形瓦片的2D渲染器,並根據格子型別改變顏色。

在這裡插入圖片描述
當需要將戰鬥表現出來時,需要將以上資料裝入一個用於顯示的“戰場”,這個戰場就是在Unity世界裡建立的。
在這裡插入圖片描述
| 在Unity中建立戰場 目前戰場的結構還非常簡單,只需要一個組織格子單位的節點。
在這裡插入圖片描述
| 建立格子 建立一個格子指令碼,並掛在一個新建立的空物件上。
在這裡插入圖片描述
為這個空物件新增一個子物件,稱之為瓦片,用來顯示一個六邊形。

因為這裡我只想做一個2D的Demo,因此我們只需要一個SpriteRenderer元件即可。

在這裡插入圖片描述
使用預先準備的一張六邊形圖片作為地塊顯示,一個格子物件就做好了。
在這裡插入圖片描述
為了區分普通和障礙,暫定普通格子為白色,障礙格子為灰色。
在這裡插入圖片描述
| 部分程式碼
在這裡插入圖片描述
鋪設普通格子
在這裡插入圖片描述
隨機放一些障礙格子

在這裡插入圖片描述
戰場根據格子資訊,顯示格子
在這裡插入圖片描述
格子根據不同型別顯示不同顏色
在這裡插入圖片描述
初始隨機多張地圖,並切換檢視。

二、新增戰場地圖功能

| 目標 實現一些後面所需的操作地圖的基礎功能,如: 1、點選戰場後高亮顯示被點中的格子; 2、點選兩個位置實現從A到B的導航並在地圖上顯示路徑; 3、設定一個移動半徑,點中格子後顯示出可移動範圍。

實現後的效果如下圖:

在這裡插入圖片描述
點選格子變紅
在這裡插入圖片描述
紅色向藍色導航(黃色為路徑,青色為探索過但沒有采用的格子)
在這裡插入圖片描述
半徑為2個單位的可移動範圍

| 點選格子變紅 我採用的方法是: 1、獲取螢幕點選位置的世界座標,並將其轉換到格子的Root節點下; 2、用這個座標推斷出點選格子所在的行、列範圍; 3、遍歷這些推測格子的中心,找到距離最近的格子,即為點中的格子。

在這裡插入圖片描述
點選螢幕後推斷的九個格子

| 調整地圖瓦片渲染器的位置 為了方便計算格子的位置,這裡調整了瓦片渲染器的位置,保證它在地塊物件的中心,這樣在設定、獲取、計算座標時,少一步轉換的操作,也更容易被理解。

在這裡插入圖片描述
| 導航 這裡採用的導航是A星演算法,網上的介紹很多,就不再贅述了。在這隻對六邊形地圖上兩格之間最短移動距離的計算做個簡單說明。

六邊形格子的路程計算

與四邊形地圖差別不大,六邊形地圖也可理解為先做行移動,再做列移動。但它的差異是:在做行移動的同時,其列也可以在一定範圍發生變化,因為它可以斜著走。

在這裡插入圖片描述
單元格座標在僅計算行移動量的同時,列可移動範圍是一個三角形。

在這裡插入圖片描述
起始點所在奇、偶行的差別,對覆蓋三角形區域的計算是有影響的。 注:黑字表示格子的列,紅字表示當前格子與出發格子的列差值。

綜上,六邊形地圖下兩格子之間的最近路程可以理解為:從起始位置先縱向移動,如果移動到與目標同行,但是目標格子又不在可到達格子範圍內的話,再橫向移動若干單位即可,如圖:

在這裡插入圖片描述
需要注意的是,奇數、偶數行與首行是否“縮排”了半個格子有關。

| 調整資料結構 導航功能交給一個新的工具類:地圖導航器來完成,但是原來地圖中格子資訊是直接儲存在戰鬥資料物件中,現在我將地圖資料從戰鬥資料中拆分出來,便於導航器集中處理資料。

在這裡插入圖片描述
舊 戰鬥資料的結構

在這裡插入圖片描述
新 戰鬥資料結構
在這裡插入圖片描述

| 根據半徑顯示可移動範圍 預設半徑顯示可移動範圍,也可轉換為在進行行偏移的同時,求列覆蓋的最小和最大值。

在這裡插入圖片描述
如在計算移動半徑為2的覆蓋區域時,假設從中心點開始,推算下方的區域,本質上是: 1、行移動量為0時,覆蓋縱座標偏移[-2~2]的區域。 2、行移動量為1時,覆蓋縱座標偏移[-1,0] + [-1, 1]的區域,其中[-1, 2]為行移動量為1的格子覆蓋區間,[-1, 1]為再橫向移動一單位時的偏移增量。 3、行移動量為2時,覆蓋縱座標偏移[-1, 1]的區域。

在計算偏移區域時,使用了上述提到的計算兩格子之間距離的方法。

三、新增對戰雙方

| 目標 向戰場中新增戰鬥單位,完成簡單的戰鬥迴圈,看起來的樣子是: 1、戰場中的對戰雙方輪流行動,可進行移動、攻擊; 2、攻擊將對敵人造成傷害; 3、沒有生命值的戰鬥單位會被從戰場中移除; 4、當一方被全部消滅時,戰鬥結束。

實現後的效果如下圖:

在這裡插入圖片描述
| 準備工作 在開始之前,我們先做一些準備工作。

顯示格子座標 為格子新增Text Mesh Pro元件以顯示格子座標,方便除錯。

在這裡插入圖片描述
增加地圖功能:放置出生點 我不想看到戰鬥單位在剛進入戰場的時候是隨機擺放位置的,因此我需要為它們提供一些出生點。這樣當戰鬥單位初入戰場時,會向戰鬥地圖請求一個本方可用的出生點,如果請求成功則加入戰場,並設定在那個位置上;如果請求失敗則不會進入戰場,避免出現亂佔位置的情況。
在這裡插入圖片描述
想象中出生點的位置,最上、最下排奇數位置放置出生點
在這裡插入圖片描述
實際生成的情況(綠色為出生點)

增加地圖功能:尋找最近可用格子 指定一個起點和一個終點,返回一個環繞終點的距離起點最近、且可用的格子。這主要是為了戰鬥單位在確定攻擊目標後,需要選擇一個它身邊的格子作為移動的目標格子(目前假定所有戰鬥單位的攻擊半徑都為1)。

這裡選擇了一種比較偷懶的方法,就是將導航位置直接設定在目標單位的身上,如果導航成功,則將到達終點的前一個格子作為目標格子,這樣不僅確定了目標格子,同時還將導航路徑一併算出。

在這裡插入圖片描述
點選起點和終點進行測試(紅:起點,藍:終點,灰:障礙,青色:目標格子)

準備工作到此為止,下面開始加入戰鬥單位。

| 新增戰鬥單位 因為是六邊形瓦片地圖組成的戰棋遊戲,因此我將戰鬥單位也表示成六邊形,目前來看兩者的Prefab並沒有什麼差別,通過設定Order值來確保戰鬥單位顯示在地圖格子的上方。

在這裡插入圖片描述
為了更好的區分戰場雙方戰鬥單位的差異,我們給它們設定不同的顏色。
在這裡插入圖片描述
雖然從顯示方式來看,戰鬥單位與地圖格子並沒有什麼差別,可是如果從資料角度出發,兩者的差別可就大了。為了更好的介紹戰鬥單位,讓我們從上至下來梳理一下整個戰場與戰鬥系統吧。

| 戰場與戰鬥資訊 一個戰場就是一場完整的戰鬥。每一個戰場目前都包含三大部分:戰場地圖、對戰雙方以及戰鬥過程。

戰場地圖 地圖在之前的文章中已經做了說明,這裡不再贅述。

對戰雙方 對戰雙方的單位是戰鬥組,這裡用戰鬥組編號區分各組,而不是僅用兩個列舉來簡單表示,是考慮到有很多組同時存在且同時對戰的情況。

真正發生戰鬥的被稱為戰鬥單位,每個戰鬥組由若干戰鬥單位組成。戰鬥資料、戰鬥組與戰鬥單位之間的關係如下圖。

在這裡插入圖片描述
戰鬥過程 戰鬥打響時,從兩個戰鬥組進入戰場,到雙方輪流移動、攻擊,最終分出勝負,發生的一切事情,都是戰鬥過程,這個後面會詳細說明。

| 戰鬥的流程 戰鬥流程包含了戰鬥的核心邏輯,是戰鬥能正常進行且完成的規則,我們用下圖來描述一場戰鬥的基本流程。

在這裡插入圖片描述
| 將資料與顯示分離 這裡還是採用了將資料與顯示分離的處理方法,先看一張資料處理的流程圖吧。
在這裡插入圖片描述
圖中很關鍵的一個內容是戰鬥過程資料,上面提及它其實是包含了自戰鬥單位進入戰場,到戰鬥最終完結之間的所有過程資料。

其實,當開始一場自動戰鬥時,戰鬥計算器會瞬時計算完整場戰鬥的過程及結果,但這些結果只是資料,並沒有呈現給玩家。

當我們需要把這場戰鬥呈現出來時,把這份資料傳遞給一個對應的顯示(播放)器即可。就好像後端和前端的分工一樣,一個負責產生資料,一個負責將資料呈現。

在這裡插入圖片描述
資料與對應的顯示器
在這裡插入圖片描述
戰鬥資料的顯示器
在這裡插入圖片描述
地圖格子顯示器
在這裡插入圖片描述
戰鬥單位顯示器

| 順序分步呈現資料 我這裡使用協同函式(Coroutine)的巢狀來分步呈現戰鬥過程。

在這裡插入圖片描述
戰場顯示器開啟逐步呈現戰鬥過程(戰鬥單位的行動)
在這裡插入圖片描述
戰鬥單位顯示器根據自己的動作資料呈現具體動作,如:
在這裡插入圖片描述
進入戰場
在這裡插入圖片描述
選擇目標並移動(青色框:發動攻擊方,黃色框:攻擊方的目標)
在這裡插入圖片描述
選擇目標並攻擊(青色框:發動攻擊方,黃色框:被攻擊方)

| 分離的意義 走吧,走吧,人總要學著自己長大。

人是這樣,資料也是。

其實直接使用一個繼承與MonoBehaviour的指令碼,把各種需要的資料都裝在裡面,直接掛在Prefab上,然後用一個控制器一邊算一邊呈現給玩家,實現起來非常容易。

但是,考慮到後臺可能有多場戰鬥同時在進行;且後期可以在短時間內進行多場戰鬥、收集資料來做戰鬥數值平衡。將資料分離,讓資料可以自行計算,就變得十分重要了。

在這裡插入圖片描述
20x20地圖下,10 vs 10的千場戰鬥結果計算,可以在很短的時間內完成

四、加入玩家控制

| 目標 分配一個角色給玩家手動操作,每回合玩家有一次行動機會,行動的規則是:可從移動、攻擊、待命中選擇一次操作,當玩家選擇移動後,還可選擇一次攻擊或待命的操作;玩家也可以不進行移動直接選擇攻擊或待命,這也會結束當前行動回合。

實現後的效果如下圖:

在這裡插入圖片描述
這次要做的非常少,因為之前已經寫好了戰鬥邏輯,只需要把手動操作加入即可。

| 準備工作 首先,為戰鬥單位設定一個是否為手動操作的標記。

在這裡插入圖片描述

其次,為戰鬥單位設定一個手動操作狀態,標記這個戰鬥單位是否可以移動、攻擊。

在這裡插入圖片描述
最後,新增一種新的戰鬥單位操作狀態和動作,分別是等待玩家操作狀態及等待手動操作的動作。
在這裡插入圖片描述
在這裡插入圖片描述
當輪到玩家操作時,戰鬥的單位會顯示綠色邊框。

| 手動操作的邏輯 大概思路是:戰場維護一個戰鬥單位的行動佇列,每當輪到一個單位行動時,如果這個單位是自動操作的,那麼它會自己決定目標,移動並攻擊,上回我們將資料與渲染進行了分離,因此它只是產生了一次行動資料(Action)。 如果這個單位是手動操作的,那它會產生一個等待手動輸入的行動資料,同時返回等待玩家操作狀態。

當戰場發現自己收到了一個等待玩家操作的狀態,就明白其實剛才的傢伙並沒有生成任何有意義的戰鬥資料,因此它必須通知自己的渲染器(那個用來顯示的模組,上回我們介紹過):嘿,幫我問問他到底想幹嘛?

渲染器通過UI與玩家進行互動,獲得玩家要移動、攻擊或是待命等“有用”的戰鬥資訊後,再回頭通知戰場“這個傢伙已經行動過了”,戰場再讓下一個單位行動。

在這裡插入圖片描述
簡易的行動流程圖

由於下一回我們才會加入戰場UI,這裡我們先用Unity自帶的GUI代替。

在這裡插入圖片描述
用GUI顯示一個列表,接收玩家的操作

五、新增常用的介面

| 目標 使用Unity自帶的UGUI替換之前的GUI來實現一些常用介面: 1、包含開始戰鬥按鈕及戰鬥結束時提示文字的主介面。 2、玩家手動操作時,輔助選擇移動、攻擊及待命的彈出皮膚。 3、點選地圖、戰鬥單位時,彈出的詳情展示皮膚。

實現後的效果如下圖:

在這裡插入圖片描述
戰鬥結束時顯示的友情提醒
在這裡插入圖片描述
行動選擇選單
在這裡插入圖片描述
戰場單位資訊皮膚

不難看出,當UGUI碰撞上專業的素材後,一個個絕美的介面瞬間躍然屏上。這也給了那些常說“程式設計師不懂美”的傢伙們一記響亮的耳光。

| 關於如何製作介面 這些介面都是用UGUI製作的,並沒有什麼難度,相信上手過UGUI或NGUI的同學,只要碰到精美的紋理貼圖,都能輕鬆完成。

在這裡插入圖片描述
使用的是一套高雅灰主題的專業UI紋理素材 本文不會對UGUI的使用做詳細介紹,我們將重點聊聊對介面的管理。 因為只要找到了方法,做出漂亮的介面就只是時間問題罷了。 而至於證明“程式設計師也能憑自己的能力作出專業介面”這件事,相信上面已經做到了。

受專案大小及時間所限,這裡會使用一套輕量級的介面管理方式。而在此之前,讓我們先做一些前期準備工作。

| 分配相機 為介面的繪製單獨分配一個相機(介面相機),並調整介面相機和戰場相機上的Clear Flags、CullingMask和Depth設定。

在這裡插入圖片描述

介面相機與戰場相機的設定

整個介面系統的根畫布(ScreenCanvas),是一個型別為ScreenSpace-Camera的Canvas,它的父節點ScreenUIRoot上有專門渲染介面的介面相機。

| 設定層級 ScreenCanvas下設5個節點,分別對應5個層級:背景層、基礎層、彈出層、頂層和Debug層.

在這裡插入圖片描述

介面的層級結構

各層的功能為: 背景層:裝飾性的、非功能性的介面。 基礎層:常駐的介面(主介面、角色頭像、快捷操作欄等)。 彈出層:點選後彈出的介面(各功能介面)。 頂層:強制顯示在最上層的介面(Tips介面或走馬燈等)。 Debug層:開發時輔助除錯用。

這些層級從下到上放置,遮擋關係是上層遮擋下層。當然,其順序、層數和名稱可根據實際需求進行調整。

需要注意的是,這5個節點(Transform)並非是必須的,它們只是為了Debug時能更直觀的檢視層級間的關係,真正用於用於區分層級的是Canvas上的SortingLayerOrderInLayer屬性。

在這裡插入圖片描述
SortingLayers設定中的層級與對應的節點
在這裡插入圖片描述
不同介面上Canvas所設定的Layer和Order值

| 介面管理流程

比起程式碼,我覺得還是看圖來的更直觀些。

在這裡插入圖片描述

介面管理流程

整個介面管理可以簡單拆解為四個部分:開啟介面、關閉介面、層級重新整理及介面重新整理,下面我們依次介紹。

| 開啟介面

在這裡插入圖片描述
開啟介面的邏輯很簡單,但需要注意的是,為了更好的使用記憶體,介面管理器維護了兩個介面快取區:常駐快取臨時快取。開啟介面時如果需要載入新的介面,先去這兩個快取區中檢視一下是否有快取過的介面,從快取區載入比重新讀取新的介面效率要高。

| 關閉介面

在這裡插入圖片描述
當我們要關閉介面時,介面管理器會根據它的“儲存策略”決定其被關閉後的去留,有些會被放入到常駐快取區,有些會被放到臨時快取區,而有些則會被直接移除。

這就好比兩性交往中女生犯錯通常當時就會被原諒;男生犯錯通常需要好好表現一段時間才有可能被原諒;而單身狗連犯錯的機會都沒有。

| 層級重新整理

在這裡插入圖片描述
上面介紹了開啟和關閉介面。其實它們都可以被看成是在“顯示介面”。因為關閉介面也可以理解為“被這個介面遮擋覆蓋的介面,可能需要顯示了”。

因此,當有介面被開啟或關閉後,介面管理器會從上到下的讓各層重新整理自己的顯示狀態及對螢幕的遮擋狀態,並將這個遮擋狀態向下傳遞,用作後面層級的顯示判斷。

| 介面重新整理 我們知道介面的重新整理和顯示是有代價的,因為它們會對CPU及GPU的效能造成開銷。因此我為每個介面設定了是否遮擋了螢幕、不可見時是否仍然重新整理及Dirty屬性

如果一個介面遮擋了螢幕,那麼它下面的介面首先應該被“隱藏”以減小渲染的壓力;其次如果不可見的介面沒有被設定為“不可見時仍然重新整理”,則當需要重新整理它時(介面資料發生了變化),也只是被打上Dirty標記,並在下次需要顯示的時候再刷

| 介面的生命週期函式 上面流程圖中的藍色部分,是介面的生命週期函式,它們會在適當的時間被介面管理器、層級或介面自身呼叫。 Init:初始化,介面首次被載入後呼叫。 OnPush:介面被顯示前,加入層級時被呼叫。 OnShow:介面被顯示時呼叫。 UpdateView:介面需要被重新整理時呼叫。 OnHide:介面被隱藏時呼叫。 OnPopup:介面被關閉,從層級中移除時呼叫。 OnExit:介面不需要被快取,被Destroy前呼叫。

在這裡插入圖片描述
一次完整的介面開啟、重新整理、關閉過程

| 介面的儲存策略 介面被關閉後會根據預設的儲存策略決定去留,這裡定義的儲存策略有以下三種。

自動移除:很少用到的介面,每次關閉時會被直接Destroy掉。

在這裡插入圖片描述

自動移除策略

臨時快取區:較為常用的介面,在關閉時我們把它放入一個有深度設定的快取區,這個快取區當收入一個新的介面時,會判斷快取量是否已超過預設深度;如果超過了預設深度,會將最早快取的介面彈出並Destroy掉。

在這裡插入圖片描述

臨時快取區策略(深度為2)

常駐快取區:需要頻繁開關的介面,在關閉時我們會把它們放入一個沒有深度設定(快取個數限制)的快取區。

在這裡插入圖片描述

常駐快取區策略(僅唯一存在介面可被設定為常駐)

| 介面配置 我使用ScriptableObject物件作為介面配置的載體。每次建立新的介面時,需要同樣建立一個配置物件,並將兩者進行關聯。介面自身及使用者可以通過讀取這個物件獲取介面的配置資訊

在這裡插入圖片描述

介面配置檔案的屬性構成

當然我們可以稍微修改Editor,新增一些輔助工具幫助我們快速生成介面配置檔案。

在這裡插入圖片描述
一個簡單的配置檔案重新整理器

六、為戰鬥單位新增血條,加入傷害文字特效

| 目標 為戰鬥單位新增血條,加入傷害文字特效。

實現後的效果如下圖:

在這裡插入圖片描述
極其精緻的血條
在這裡插入圖片描述
與3A遊戲同款的傷害文字特效 | 新增血條 新增一個簡單的血條,主要由三部分構成:紅色的底,綠色的條和生命值。

偷個懶,直接使用SpriteRenderer + TextMesh Pro來完成它。

在這裡插入圖片描述
一個簡單的血條

每次生命值變化時需要更新兩個東西:綠條的長度和生命值。

更新文字很容易,直接設定即可;更新綠條的長度呢?只要調整它在x方向上的縮放比例就行了。

在這裡插入圖片描述
通過Scale X來控制綠條的長度

但是需要注意將Sprite的錨點設定為Left,這樣僅調整x方向的縮放比例就能達到目的;如果錨點為Center的話,還需要同時調整Position X。

在這裡插入圖片描述
血條所用圖片的匯入設定

| 新增傷害文字特效 好吧,再偷個懶,直接用一個TextMesh Pro配合一個Animator即可...

在這裡插入圖片描述
一個簡單的傷害文字特效

做法很簡單,將文字的動畫直接做在一個Animation Clip中,然後用Animator Controller控制播放它們就行了。

在這裡插入圖片描述
錄製動畫

在這裡我做了兩個動畫:一個普通傷害的動畫,和一個暴擊傷害的動畫(後面會用到)。當然,它們之間的顯示效果差別非常大。

在這裡插入圖片描述
普通傷害文字
在這裡插入圖片描述
暴擊傷害文字

後面就非常簡單了,在Animator皮膚中設定動畫播放規則,比如通過不同的Trigger來播放普通傷害特效或暴擊傷害特效,然後再在程式碼中根據傷害型別設定對應的Trigger即可。

在這裡插入圖片描述
用Trigger來區分這個Animator應該播放哪個動畫
在這裡插入圖片描述
播放動畫

七、擴充套件作戰單位

豐富戰鬥元素,加入並實現手動釋放不同型別的技能。 | 目標 加入一些常見、簡單的技能型別,如:

1、單體遠端 對遠端一個敵方單位進行攻擊。

在這裡插入圖片描述
遠端攻擊單體目標

2、單體遠端帶範圍效果 對遠端一個敵方單位進行攻擊,同時對其周圍一定距離內所有敵方單位造成相同傷害。

在這裡插入圖片描述
單體遠端帶範圍效果

3、以自身為中心的範圍技能 以自身為中心,對周圍一定距離內所有敵方單位造成傷害。

在這裡插入圖片描述
以自身為中心的範圍技能

4、遠端指定範圍的技能 遠端指定攻擊一定範圍內的所有敵方單位。

在這裡插入圖片描述
遠端指定範圍的技能

5、單體恢復 恢復自己或一個友方單位的HP值。

在這裡插入圖片描述
單體恢復

需要提前宣告的是,本文主要記錄的是在手動釋放技能時,操作展示上的一些關鍵事項;技能計算的邏輯請見程式碼;AI釋放不同型別技能也將放在下回。

| 增加效果顯示 我為地塊、戰鬥單位設定了不同的顯示狀態以便更直觀的獲取操作反饋。

在這裡插入圖片描述
戰鬥單位、地塊的顯示狀態

無論是地塊還是戰鬥單位,都是通過簡單的狀態機來實現不同顯示效果的切換。

| 地塊的效果顯示 由於地塊會包含多種狀態共存的情況,比如上面的遠端範圍攻擊:某些地塊會被同時設定為技能釋放範圍和技能效果覆蓋範圍。

在這裡插入圖片描述
部分地塊既在技能釋放範圍內,又在技能效果範圍內

為了解決這種情況,地塊的顯示狀態判斷使用了位運算。

在這裡插入圖片描述
使用位運算來控制顯示狀態

| 戰鬥單位的效果顯示 戰鬥單位不涉及多種狀態同時存在的情況,處理起來就簡單多了。

在這裡插入圖片描述
戰鬥單位的顯示狀態

| 技能資訊 與之前的介面配置一樣,我仍然使用ScriptableObject作為技能資訊的載體,因為這樣實現起來最快捷。

在這裡插入圖片描述
記錄技能資訊的ScriptableObject

由於目前包含的技能型別較少,數值計算也十分簡單,因此只需要少量的屬性就足夠了,這裡就不再贅述了。

| 手選技能的操作規則 其實,這些技能的計算邏輯並不複雜,麻煩一些的是不同型別技能在手動操作時的規則及顯示邏輯。 我在這裡制定了簡單的操作規則:

1、對於單體近戰、單體遠端、單體恢復技能: 選擇技能後顯示技能釋放範圍,標出範圍內外的單位;點選可選單位則釋放技能。

在這裡插入圖片描述
對單體目標技能的操作

2、對於單體遠端帶範圍效果的技能: 選擇技能後顯示技能釋放範圍,標出範圍內外的單位;點選可選單位後展示技能效果範圍,標出範圍內的單位;再次點選該單位後釋放技能。

在這裡插入圖片描述
對單體遠端帶範圍效果技能的操作

3、對於以自身為中心的範圍技能: 選擇技能後顯示技能效果範圍,標出範圍內外的單位;點選任意單位後釋放技能。

在這裡插入圖片描述
對以自身為中心的範圍技能的操作

4、對於遠端指定範圍的技能: 選擇技能後顯示技能釋放範圍;點選範圍內任意地塊,顯示技能效果範圍,標出範圍內外的單位;再次點選相同地塊釋放技能。

在這裡插入圖片描述

對遠端指定範圍技能的操作

5、操作任意型別技能時,點選滑鼠右鍵為取消。

| 簡單的技能分析 為了實現上述操作邏輯,我在點選使用技能後加入了一個簡單的技能分析步驟,它會遍歷場上所有戰鬥單位,並根據釋放者及技能型別將他們劃分為可被選中的、隊伍不符的、距離不符的和狀態異常的四類,這樣後面的操作邏輯實現起來就簡單多了。

在這裡插入圖片描述
點選使用技能後的流程
在這裡插入圖片描述
技能分析結果

| 增加技能操作介面 我微調了戰鬥單位的操作皮膚,為攻擊按鈕增加了一個選擇技能的二級皮膚。新增了一個一級皮膚透明處理的小設定來區分層級;以及在不同螢幕位置點選時的皮膚彈出位置的優化,以防止皮膚彈出到螢幕以外無法操作。由於這不是本次介紹的重點,就不在這贅述了。

在這裡插入圖片描述
技能選擇皮膚

八、加入AI系統(上)

建立超級簡單的AI系統。 | 目標 加入一個超級簡單的AI系統,會自動釋放不同型別的傷害技能。

在這裡插入圖片描述

自動釋放技能的AI

需要提前說明的是,建立簡單的AI系統預計將拆分為三篇更新。

第一篇(本篇)通過加入一些簡單的AI邏輯,保證戰鬥單位可以自動選擇(傷害)技能、自動作戰,進而順利的完成一場戰鬥。

第二篇會進一步豐富AI的決策系統,讓它的表現更具期待性,使戰鬥變得更加有趣。

此外,我邀請了我的好友Aillieo,拜託他按照自己的方式也設計一個AI系統。

因此,我會在第三篇介紹他所設計的AI系統,並對這三篇做一個整體的總結。

| 非常簡單的AI系統 個人以為,有意思的AI系統可以簡單的定義為:

讓人覺得符合邏輯,卻又在一定程度上超出了預期。

如何實現一個非常簡單的AI系統呢?為了讓問題變得再簡單些,我將AI的行為拆解成固定的三個步驟: 1、確定攻擊目標; 2、向攻擊目標移動; 3、使用技能。

| 確定攻擊目標 將”合理“的目標設定為攻擊目標,是件並不太容易的事情。

這裡我且不談那些優秀的遊戲是怎麼做的,因為我也不知道。只說說我目前所使用的方法:仇恨系統。

在這裡插入圖片描述

AI使用仇恨列表確定攻擊目標

每當一個戰鬥單位在戰場中被敵人攻擊時,他就會偷偷的在自己的小本本里記下攻擊者的名字,以及他們的罪行。

當輪到他行動時,他就會掏出自己攥了很久的小本本,按照之前它們揍自己的程度進行降序排列,然後按照這個名單,判斷自己反擊的可能性。

這裡,沒有反擊的可能性,指的是:如果目標已經被人包圍,自己卻又是一個近戰角色無法靠近,那他就會嘟囔著“哼饒你一條狗命”,然後繼續看下一個人。

直到確定這個傢伙可以被自己攻擊到,他就會合上小本本,把他的名字刻上自己的心頭,然後準備開始下一個步驟:向他移動。

| 向目標單位移動 向目標移動就很簡單了,通過A-Star演算法找到移動路徑後,行動即可。

在這裡插入圖片描述
確定目標後沿路徑移動

但是這裡有一個小問題:應該選擇哪個格子作為移動的終點呢?

特別是當攻擊者是某些遠端攻擊單位,比如遊戲中常見的魔法師或者弓箭手,每次都走到目標旁邊去攻擊,感覺上就有點像“送外賣”。

其實解決方法也很簡單,在導航時仍然選擇目標所在位置做為導航終點,但在距離終點一定距離時,停止導航並返回導航路徑即可。這個停止距離,就是遠端攻擊單位的射程,或者手動設定的某個值。

在這裡插入圖片描述

射程為2的小紅,導航停止在距離小藍兩個單位的格子上

這與“真正的愛情,能跨越一切障礙”是一個道理。

當然,如果這個人兒並不在天邊,而在觸手可及的地方,那他根本就不用移動,直接進入下面的環節吧。

| 對目標使用技能 光說,不練,假把式。

好容易走到了他(她)的身邊,總得有所表示吧?

試想一個場景:你很喜歡一個女孩兒,在表白的關鍵時刻,你有一百種表達方法,但你卻只能選擇一種,究竟哪種才是最有效的呢?

如果是真實的生活,答案很簡單:看運氣。

但是遊戲則不同,你可以用S/L大法(存、讀檔大法)來不斷重試,直到找出效果最好的那一種!

決策將要使用的技能也可以是一樣的。

這裡我且不談那些優秀的遊戲是怎麼做的,因為我也不知道。只說說我目前所使用的方法:簡單的計算所有可用技能的釋放回報。

在這裡插入圖片描述
計算技能得分並確定所使用的技能

計算技能釋放得分的公式異常複雜,由於這並不是一篇學術性論文,因此這裡不做詳細的解釋和說明,只把公式列出即可:

技能釋放得分 = 技能造成的總傷害 ÷ 技能消耗的能量值

也就眾所周知的:

從零點五開始用Unity做半個2D戰棋小遊戲(八)

天啊,好麻煩。

但是,在得到了按照釋放得分降序排列的可用技能列表後,帶著何種的心情、用著怎樣的姿勢、使用哪個技能的問題,就變得十分容易了。

可能我們只需要注意下遠端範圍技能的釋放點選擇問題即可。

從零點五開始用Unity做半個2D戰棋小遊戲(八)
釋放影響半徑為2的遠端範圍技能

對於遠端範圍技能,我們當然可以使用一些方法,找到覆蓋最多目標的釋放點。

但為了省事兒,我這裡是這麼處理的:當目標超過技能釋放距離時,嘗試找到釋放技能時,可以覆蓋到目標單位的點,然後從這裡隨便選一個即可。當然,如果目標本身就在技能釋放半徑內,就選它為釋放中心了。

從零點五開始用Unity做半個2D戰棋小遊戲(八)
紅色區域為覆蓋半徑為2的技能在釋放時,可以傷害到藍色格子的釋放點

| 能量值 當然,為了幫助AI計算出哪個技能的釋放得分更高,我為每個戰鬥單位都增加了一個能量值的屬性(你也可以認為它是魔法值);為每個技能增加了釋放的能量消耗;同時還為遊戲增加了每次行動時恢復10個單位能量的設定。但是由於這些邏輯都很簡單,這裡就不贅述了。

最後,我們來回顧下整個行動流程吧: 1、打誰; 2、去哪打; 3、怎麼打。

從零點五開始用Unity做半個2D戰棋小遊戲(八)
完整的AI行動流程

| 寫在最後 至此,建立超級簡單的AI系統篇就介紹到這了。如你所見,這裡只是實現了非常簡單的AI行動邏輯,並沒有體現出各種型別AI的不同,我們下期將嘗試著解決這個問題。

相關文章