此次想要作的一個小遊戲,或者說一個小Demo,實際上是一個簡單且傳統的戰棋戰鬥場景。初步的設計是:在2D世界裏建立一張由六邊形地塊組成的戰鬥地圖,敵我雙方依據體力在地圖上輪流行動並向對方發動攻擊,先消滅掉全部敵人的一方將得到勝利。前端
這一輯將比上一輯的內容更簡單,但完成後會是一個功能較完整且能夠玩耍的Demo。git
我使用的Unity版本是2018.2.7,可是其實並無用到2018的任何新功能。程序員
Github:github.com/elsong823/H… *能夠本身下載源代碼github
預計將分爲如下幾篇:算法
一、建立戰場 根據預約尺寸生成戰場地圖,並隨機一些障礙物。後端
二、添加對戰雙方 向戰場中添加做戰單位,做戰單位可被點選,並進行移動。數組
三、戰場邏輯 做戰雙方按照順序依次行動,可進行移動、攻擊。緩存
四、添加戰場UI 添加能夠隨時顯示戰況的Hud、爲做戰單位添加血條等。數據結構
五、擴展做戰單位 豐富做戰單位的類型,添加職業,並加入若干不一樣類型的技能。ide
六、擴展戰場地圖 豐富戰場地圖,加入地形及道具等元素。
七、規範戰鬥配置 能夠經過規範化的數據結構配置戰場、職業、技能、道具等。
| 目標 生成一個規定尺寸的戰場,戰場上的格子均爲六邊形,而且暫定兩種格子類型:普通格子和障礙格子。
| 在開始以前我作了如下事情 1.建立了新的工程。 2.新建場景並保存在場景文件夾下。 3.刪除默認建立的平行光,調整場景光照設置,棄用了天空盒等。 4.將相機調整爲正交相機。 由於是從零點五開始系列,認爲讀者有必定的Unity引擎使用基礎,就不對這些操做進行介紹了。
| 建立戰場地圖 我沒有采用把數據和顯示捆綁在一塊兒的作法,而是將數據和顯示分離。 個人思路是用一個戰鬥建立器來建立戰鬥,一場完整的戰鬥信息至少應包含地圖及對戰雙方的信息。而Unity搭建的戰鬥場景則能夠理解爲一場戰鬥的顯示器。
| 數據部分(主要) 戰鬥數據:地圖尺寸,包含全部格子的二維數組。 格子數據:格子類型,所在行列,所在空間座標。
由於這裏我只想作一個2D的Demo,所以咱們只須要一個SpriteRenderer組件便可。
| 目標 實現一些後面所需的操做地圖的基礎功能,如: 一、點擊戰場後高亮顯示被點中的格子; 二、點擊兩個位置實現從A到B的導航並在地圖上顯示路徑; 三、設定一個移動半徑,點中格子後顯示出可移動範圍。
實現後的效果以下圖:
| 點擊格子變紅 我採用的方法是: 一、獲取屏幕點擊位置的世界座標,並將其轉換到格子的Root節點下; 二、用這個座標推斷出點擊格子所在的行、列範圍; 三、遍歷這些推測格子的中心,找到距離最近的格子,即爲點中的格子。
| 調整地圖瓦片渲染器的位置 爲了方便計算格子的位置,這裏調整了瓦片渲染器的位置,保證它在地塊對象的中心,這樣在設置、獲取、計算座標時,少一步轉換的操做,也更容易被理解。
六邊形格子的路程計算
與四邊形地圖差異不大,六邊形地圖也可理解爲先作行移動,再作列移動。但它的差別是:在作行移動的同時,其列也能夠在必定範圍發生變化,由於它能夠斜着走。
綜上,六邊形地圖下兩格子之間的最近路程能夠理解爲:從起始位置先縱向移動,若是移動到與目標同行,可是目標格子又不在可到達格子範圍內的話,再橫向移動若干單位便可,如圖:
| 調整數據結構 導航功能交給一個新的工具類:地圖導航器來完成,可是原來地圖中格子信息是直接保存在戰鬥數據對象中,如今我將地圖數據從戰鬥數據中拆分出來,便於導航器集中處理數據。
| 根據半徑顯示可移動範圍 預設半徑顯示可移動範圍,也可轉換爲在進行行偏移的同時,求列覆蓋的最小和最大值。
在計算偏移區域時,使用了上述提到的計算兩格子之間距離的方法。
| 目標 向戰場中添加戰鬥單位,完成簡單的戰鬥循環,看起來的樣子是: 一、戰場中的對戰雙方輪流行動,可進行移動、攻擊; 二、攻擊將對敵人形成傷害; 三、沒有生命值的戰鬥單位會被從戰場中移除; 四、當一方被所有消滅時,戰鬥結束。
實現後的效果以下圖:
顯示格子座標 爲格子添加Text Mesh Pro組件以顯示格子座標,方便調試。
增長地圖功能:尋找最近可用格子 指定一個起點和一個終點,返回一個環繞終點的距離起點最近、且可用的格子。這主要是爲了戰鬥單位在肯定攻擊目標後,須要選擇一個它身邊的格子做爲移動的目標格子(目前假定全部戰鬥單位的攻擊半徑都爲1)。
這裏選擇了一種比較偷懶的方法,就是將導航位置直接設定在目標單位的身上,若是導航成功,則將到達終點的前一個格子做爲目標格子,這樣不只肯定了目標格子,同時還將導航路徑一併算出。
準備工做到此爲止,下面開始加入戰鬥單位。
| 添加戰鬥單位 由於是六邊形瓦片地圖組成的戰棋遊戲,所以我將戰鬥單位也表示成六邊形,目前來看二者的Prefab並無什麼差異,經過設置Order值來確保戰鬥單位顯示在地圖格子的上方。
| 戰場與戰鬥信息 一個戰場就是一場完整的戰鬥。每個戰場目前都包含三大部分:戰場地圖、對戰雙方以及戰鬥過程。
戰場地圖 地圖在以前的文章中已經作了說明,這裏再也不贅述。
對戰雙方 對戰雙方的單位是戰鬥組,這裏用戰鬥組編號區分各組,而不是僅用兩個枚舉來簡單表示,是考慮到有不少組同時存在且同時對戰的狀況。
真正發生戰鬥的被稱爲戰鬥單位,每一個戰鬥組由若干戰鬥單位組成。戰鬥數據、戰鬥組與戰鬥單位之間的關係以下圖。
| 戰鬥的流程 戰鬥流程包含了戰鬥的核心邏輯,是戰鬥能正常進行且完成的規則,咱們用下圖來描述一場戰鬥的基本流程。
其實,當開始一場自動戰鬥時,戰鬥計算器會瞬時計算完整場戰鬥的過程及結果,但這些結果只是數據,並無呈現給玩家。
當咱們須要把這場戰鬥呈現出來時,把這份數據傳遞給一個對應的顯示(播放)器便可。就好像後端和前端的分工同樣,一個負責產生數據,一個負責將數據呈現。
| 順序分步呈現數據 我這裏使用協同函數(Coroutine)的嵌套來分步呈現戰鬥過程。
| 分離的意義 走吧,走吧,人總要學着本身長大。
人是這樣,數據也是。
其實直接使用一個繼承與MonoBehaviour的腳本,把各類須要的數據都裝在裏面,直接掛在Prefab上,而後用一個控制器一邊算一邊呈現給玩家,實現起來很是容易。
可是,考慮到後臺可能有多場戰鬥同時在進行;且後期能夠在短期內進行多場戰鬥、收集數據來作戰鬥數值平衡。將數據分離,讓數據能夠自行計算,就變得十分重要了。
| 目標 分配一個角色給玩家手動操做,每回合玩家有一次行動機會,行動的規則是:可從移動、攻擊、待命中選擇一次操做,當玩家選擇移動後,還可選擇一次攻擊或待命的操做;玩家也能夠不進行移動直接選擇攻擊或待命,這也會結束當前行動回合。
實現後的效果以下圖:
| 準備工做 首先,爲戰鬥單位設置一個是否爲手動操做的標記。
其次,爲戰鬥單位設定一個手動操做狀態,標記這個戰鬥單位是否能夠移動、攻擊。
| 手動操做的邏輯 大概思路是:戰場維護一個戰鬥單位的行動隊列,每當輪到一個單位行動時,若是這個單位是自動操做的,那麼它會本身決定目標,移動並攻擊,上回咱們將數據與渲染進行了分離,所以它只是產生了一次行動數據(Action)。 若是這個單位是手動操做的,那它會產生一個等待手動輸入的行動數據,同時返回等待玩家操做狀態。
當戰場發現本身收到了一個等待玩家操做的狀態,就明白其實剛纔的傢伙並無生成任何有意義的戰鬥數據,所以它必須通知本身的渲染器(那個用來顯示的模塊,上回咱們介紹過):嘿,幫我問問他到底想幹嗎?
渲染器經過UI與玩家進行交互,得到玩家要移動、攻擊或是待命等「有用」的戰鬥信息後,再回頭通知戰場「這個傢伙已經行動過了」,戰場再讓下一個單位行動。
因爲下一回咱們纔會加入戰場UI,這裏咱們先用Unity自帶的GUI代替。
| 目標 使用Unity自帶的UGUI替換以前的GUI來實現一些經常使用界面: 一、包含開始戰鬥按鈕及戰鬥結束時提示文字的主界面。 二、玩家手動操做時,輔助選擇移動、攻擊及待命的彈出面板。 三、點選地圖、戰鬥單位時,彈出的詳情展現面板。
實現後的效果以下圖:
不難看出,當UGUI碰撞上專業的素材後,一個個絕美的界面瞬間躍然屏上。這也給了那些常說「程序員不懂美」的傢伙們一記響亮的耳光。
| 關於如何製做界面 這些界面都是用UGUI製做的,並無什麼難度,相信上手過UGUI或NGUI的同窗,只要碰到精美的紋理貼圖,都能輕鬆完成。
受項目大小及時間所限,這裏會使用一套輕量級的界面管理方式。而在此以前,讓咱們先作一些前期準備工做。
| 分配相機 爲界面的繪製單獨分配一個相機(界面相機),並調整界面相機和戰場相機上的Clear Flags、CullingMask和Depth設置。
整個界面系統的根畫布(ScreenCanvas),是一個類型爲ScreenSpace-Camera的Canvas,它的父節點ScreenUIRoot上有專門渲染界面的界面相機。
| 設置層級 ScreenCanvas下設5個節點,分別對應5個層級:背景層、基礎層、彈出層、頂層和Debug層.
各層的功能爲: 背景層:裝飾性的、非功能性的界面。 基礎層:常駐的界面(主界面、角色頭像、快捷操做欄等)。 彈出層:點擊後彈出的界面(各功能界面)。 頂層:強制顯示在最上層的界面(Tips界面或走馬燈等)。 Debug層:開發時輔助調試用。
這些層級從下到上放置,遮擋關係是上層遮擋下層。固然,其順序、層數和名稱可根據實際需求進行調整。
須要注意的是,這5個節點(Transform)並不是是必須的,它們只是爲了Debug時能更直觀的查看層級間的關係,真正用於用於區分層級的是Canvas上的SortingLayer和OrderInLayer屬性。
| 界面管理流程
比起代碼,我以爲仍是看圖來的更直觀些。
整個界面管理能夠簡單拆解爲四個部分:打開界面、關閉界面、層級刷新及界面刷新,下面咱們依次介紹。
| 打開界面
| 關閉界面
這就比如兩性交往中女生犯錯一般當時就會被原諒;男生犯錯一般須要好好表現一段時間纔有可能被原諒;而單身狗連犯錯的機會都沒有。
| 層級刷新
所以,當有界面被打開或關閉後,界面管理器會從上到下的讓各層刷新本身的顯示狀態及對屏幕的遮擋狀態,並將這個遮擋狀態向下傳遞,用做後面層級的顯示判斷。
| 界面刷新 咱們知道界面的刷新和顯示是有代價的,由於它們會對CPU及GPU的性能形成開銷。所以我爲每一個界面設置了是否遮擋了屏幕、不可見時是否仍然刷新及Dirty屬性。
若是一個界面遮擋了屏幕,那麼它下面的界面首先應該被「隱藏」以減少渲染的壓力;其次若是不可見的界面沒有被設置爲「不可見時仍然刷新」,則當須要刷新它時(界面數據發生了變化),也只是被打上Dirty標記,並在下次須要顯示的時候再刷新。
| 界面的生命週期函數 上面流程圖中的藍色部分,是界面的生命週期函數,它們會在適當的時間被界面管理器、層級或界面自身調用。 Init:初始化,界面首次被加載後調用。 OnPush:界面被顯示前,加入層級時被調用。 OnShow:界面被顯示時調用。 UpdateView:界面須要被刷新時調用。 OnHide:界面被隱藏時調用。 OnPopup:界面被關閉,從層級中移除時調用。 OnExit:界面不須要被緩存,被Destroy前調用。
| 界面的存儲策略 界面被關閉後會根據預設的存儲策略決定去留,這裏定義的存儲策略有如下三種。
自動移除:不多用到的界面,每次關閉時會被直接Destroy掉。
臨時緩存區:較爲經常使用的界面,在關閉時咱們把它放入一個有深度設置的緩存區,這個緩存區當收入一個新的界面時,會判斷緩存量是否已超過預設深度;若是超過了預設深度,會將最先緩存的界面彈出並Destroy掉。
常駐緩存區:須要頻繁開關的界面,在關閉時咱們會把它們放入一個沒有深度設置(緩存個數限制)的緩存區。
| 界面配置 我使用ScriptableObject對象做爲界面配置的載體。每次建立新的界面時,須要一樣建立一個配置對象,並將二者進行關聯。界面自身及使用者能夠經過讀取這個對象獲取界面的配置信息。
固然咱們能夠稍微修改Editor,添加一些輔助工具幫助咱們快速生成界面配置文件。
| 目標 爲戰鬥單位添加血條,加入傷害文字特效。
實現後的效果以下圖:
偷個懶,直接使用SpriteRenderer + TextMesh Pro來完成它。
每次生命值變化時須要更新兩個東西:綠條的長度和生命值。
更新文字很容易,直接設置便可;更新綠條的長度呢?只要調整它在x方向上的縮放比例就好了。
可是須要注意將Sprite的錨點設置爲Left,這樣僅調整x方向的縮放比例就能達到目的;若是錨點爲Center的話,還須要同時調整Position X。
| 添加傷害文字特效 好吧,再偷個懶,直接用一個TextMesh Pro配合一個Animator便可...
作法很簡單,將文字的動畫直接作在一個Animation Clip中,而後用Animator Controller控制播放它們就好了。
在這裏我作了兩個動畫:一個普通傷害的動畫,和一個暴擊傷害的動畫(後面會用到)。固然,它們之間的顯示效果差異很是大。
後面就很是簡單了,在Animator面板中設置動畫播放規則,好比經過不一樣的Trigger來播放普通傷害特效或暴擊傷害特效,而後再在代碼中根據傷害類型設置對應的Trigger便可。
豐富戰鬥元素,加入並實現手動釋放不一樣類型的技能。 | 目標 加入一些常見、簡單的技能類型,如:
一、單體遠程 對遠程一個敵方單位進行攻擊。
二、單體遠程帶範圍效果 對遠程一個敵方單位進行攻擊,同時對其周圍必定距離內全部敵方單位形成相同傷害。
三、以自身爲中心的範圍技能 以自身爲中心,對周圍必定距離內全部敵方單位形成傷害。
四、遠程指定範圍的技能 遠程指定攻擊必定範圍內的全部敵方單位。
五、單體恢復 恢復本身或一個友方單位的HP值。
須要提早聲明的是,本文主要記錄的是在手動釋放技能時,操做展現上的一些關鍵事項;技能計算的邏輯請見代碼;AI釋放不一樣類型技能也將放在下回。
| 增長效果顯示 我爲地塊、戰鬥單位設置了不一樣的顯示狀態以便更直觀的獲取操做反饋。
不管是地塊仍是戰鬥單位,都是經過簡單的狀態機來實現不一樣顯示效果的切換。
| 地塊的效果顯示 因爲地塊會包含多種狀態共存的狀況,好比上面的遠程範圍攻擊:某些地塊會被同時設置爲技能釋放範圍和技能效果覆蓋範圍。
爲了解決這種狀況,地塊的顯示狀態判斷使用了位運算。
| 戰鬥單位的效果顯示 戰鬥單位不涉及多種狀態同時存在的狀況,處理起來就簡單多了。
| 技能信息 與以前的界面配置同樣,我仍然使用ScriptableObject做爲技能信息的載體,由於這樣實現起來最快捷。
因爲目前包含的技能類型較少,數值計算也十分簡單,所以只須要少許的屬性就足夠了,這裏就再也不贅述了。
| 手選技能的操做規則 其實,這些技能的計算邏輯並不複雜,麻煩一些的是不一樣類型技能在手動操做時的規則及顯示邏輯。 我在這裏制定了簡單的操做規則:
一、對於單體近戰、單體遠程、單體恢復技能: 選擇技能後顯示技能釋放範圍,標出範圍內外的單位;點擊可選單位則釋放技能。
二、對於單體遠程帶範圍效果的技能: 選擇技能後顯示技能釋放範圍,標出範圍內外的單位;點擊可選單位後展現技能效果範圍,標出範圍內的單位;再次點擊該單位後釋放技能。
三、對於以自身爲中心的範圍技能: 選擇技能後顯示技能效果範圍,標出範圍內外的單位;點擊任意單位後釋放技能。
四、對於遠程指定範圍的技能: 選擇技能後顯示技能釋放範圍;點擊範圍內任意地塊,顯示技能效果範圍,標出範圍內外的單位;再次點擊相同地塊釋放技能。
對遠程指定範圍技能的操做
五、操做任意類型技能時,點擊鼠標右鍵爲取消。
| 簡單的技能分析 爲了實現上述操做邏輯,我在點擊使用技能後加入了一個簡單的技能分析步驟,它會遍歷場上全部戰鬥單位,並根據釋放者及技能類型將他們劃分爲可被選中的、隊伍不符的、距離不符的和狀態異常的四類,這樣後面的操做邏輯實現起來就簡單多了。
| 增長技能操做界面 我微調了戰鬥單位的操做面板,爲攻擊按鈕增長了一個選擇技能的二級面板。添加了一個一級面板透明處理的小設置來區分層級;以及在不一樣屏幕位置點擊時的面板彈出位置的優化,以防止面板彈出到屏幕之外沒法操做。因爲這不是本次介紹的重點,就不在這贅述了。
創建超級簡單的AI系統。 | 目標 加入一個超級簡單的AI系統,會自動釋放不一樣類型的傷害技能。
自動釋放技能的AI
須要提早說明的是,創建簡單的AI系統預計將拆分爲三篇更新。
第一篇(本篇)經過加入一些簡單的AI邏輯,保證戰鬥單位能夠自動選擇(傷害)技能、自動做戰,進而順利的完成一場戰鬥。
第二篇會進一步豐富AI的決策系統,讓它的表現更具期待性,使戰鬥變得更加有趣。
此外,我邀請了個人好友Aillieo,拜託他按照本身的方式也設計一個AI系統。
所以,我會在第三篇介紹他所設計的AI系統,並對這三篇作一個總體的總結。
| 很是簡單的AI系統 我的覺得,有意思的AI系統能夠簡單的定義爲:
讓人以爲符合邏輯,卻又在必定程度上超出了預期。
如何實現一個很是簡單的AI系統呢?爲了讓問題變得再簡單些,我將AI的行爲拆解成固定的三個步驟: 一、肯定攻擊目標; 二、向攻擊目標移動; 三、使用技能。
| 肯定攻擊目標 將」合理「的目標設定爲攻擊目標,是件並不太容易的事情。
這裏我且不談那些優秀的遊戲是怎麼作的,由於我也不知道。只說說我目前所使用的方法:仇恨系統。
AI使用仇恨列表肯定攻擊目標
每當一個戰鬥單位在戰場中被敵人攻擊時,他就會偷偷的在本身的小本本里記下攻擊者的名字,以及他們的罪行。
當輪到他行動時,他就會掏出本身攥了好久的小本本,按照以前它們揍本身的程度進行降序排列,而後按照這個名單,判斷本身反擊的可能性。
這裏,沒有反擊的可能性,指的是:若是目標已經被人包圍,本身卻又是一個近戰角色沒法靠近,那他就會嘟囔着「哼饒你一條狗命」,而後繼續看下一我的。
直到肯定這個傢伙能夠被本身攻擊到,他就會合上小本本,把他的名字刻上本身的心頭,而後準備開始下一個步驟:向他移動。
| 向目標單位移動 向目標移動就很簡單了,經過A-Star算法找到移動路徑後,行動便可。
可是這裏有一個小問題:應該選擇哪一個格子做爲移動的終點呢?
特別是當攻擊者是某些遠程攻擊單位,好比遊戲中常見的魔法師或者弓箭手,每次都走到目標旁邊去攻擊,感受上就有點像「送外賣」。
其實解決方法也很簡單,在導航時仍然選擇目標所在位置作爲導航終點,但在距離終點必定距離時,中止導航並返回導航路徑便可。這個中止距離,就是遠程攻擊單位的射程,或者手動設定的某個值。
射程爲2的小紅,導航中止在距離小藍兩個單位的格子上
這與「真正的愛情,能跨越一切障礙」是一個道理。
固然,若是這我的兒並不在天邊,而在觸手可及的地方,那他根本就不用移動,直接進入下面的環節吧。
| 對目標使用技能 光說,不練,假把式。
好容易走到了他(她)的身邊,總得有所表示吧?
試想一個場景:你很喜歡一個女孩兒,在表白的關鍵時刻,你有一百種表達方法,但你卻只能選擇一種,究竟哪一種纔是最有效的呢?
若是是真實的生活,答案很簡單:看運氣。
可是遊戲則不一樣,你能夠用S/L大法(存、讀檔大法)來不斷重試,直到找出效果最好的那一種!
決策將要使用的技能也能夠是同樣的。
這裏我且不談那些優秀的遊戲是怎麼作的,由於我也不知道。只說說我目前所使用的方法:簡單的計算全部可用技能的釋放回報。
計算技能釋放得分的公式異常複雜,因爲這並非一篇學術性論文,所以這裏不作詳細的解釋和說明,只把公式列出便可:
技能釋放得分 = 技能形成的總傷害 ÷ 技能消耗的能量值
也就衆所周知的:
天啊,好麻煩。
可是,在獲得了按照釋放得分降序排列的可用技能列表後,帶着何種的心情、用着怎樣的姿式、使用哪一個技能的問題,就變得十分容易了。
可能咱們只須要注意下遠程範圍技能的釋放點選擇問題便可。
對於遠程範圍技能,咱們固然可使用一些方法,找到覆蓋最多目標的釋放點。
但爲了省事兒,我這裏是這麼處理的:當目標超過技能釋放距離時,嘗試找到釋放技能時,能夠覆蓋到目標單位的點,而後從這裏隨便選一個便可。固然,若是目標自己就在技能釋放半徑內,就選它爲釋放中心了。
| 能量值 固然,爲了幫助AI計算出哪一個技能的釋放得分更高,我爲每一個戰鬥單位都增長了一個能量值的屬性(你也能夠認爲它是魔法值);爲每一個技能增長了釋放的能量消耗;同時還爲遊戲增長了每次行動時恢復10個單位能量的設定。可是因爲這些邏輯都很簡單,這裏就不贅述了。
最後,咱們來回顧下整個行動流程吧: 一、打誰; 二、去哪打; 三、怎麼打。
| 寫在最後 至此,創建超級簡單的AI系統篇就介紹到這了。如你所見,這裏只是實現了很是簡單的AI行動邏輯,並無體現出各類類型AI的不一樣,咱們下期將嘗試着解決這個問題。