3D 沙盒遊戲之地面網格設計

背景

最近小組在探索研發一個 3D 的沙盒小遊戲 demo。對於沙盒遊戲來說,地面是必不可少的元素。爲了降低難度,在這個 demo 中,地面將不涉及 y 軸座標的變化,也就是使用一個與 xOz 平面平行的平面,對應到現實世界中,就是一塊不帶任何起伏的平地。本篇文章以 babylon.js 作爲框架進行說明。期望的效果類似下圖(截圖來自於手遊部落衝突):


目標

首先我們需要在 xOz 平面上創建一塊矩形作爲地面。爲了不讓地面看起來過於單調,需要給地面貼上一些紋理,比如草地、鵝卵石路等等;在此基礎上,紋理還需要可以局部替換,比如可以實現一條在草地中央的鵝卵石小路。同時,在地面上,需要放置其他模型(比如人物、建築等),爲了避免模型在移動或者新增的時候,出現重疊的情況,還得知道當前地面上對應的位置的狀態(是否已被模型佔用),因此在新增或移動模型的時候,需要獲取當前模型在地面上的具體位置信息。基於以上需求,可以梳理爲以下兩個大目標:

  1. 完成地面初始化,且可以改變特定位置的紋理
  2. 獲取模型在地面上的位置信息

圍繞這兩個目標,下面通過兩個實現篇,給大家展示下如何一步步實現~


實現之地面創建篇

首先要把思路捋一下:先是需要創建一個地面,其實地面的本質也是一個模型。其次要修改地面的部分紋理。有一個比較簡單的方法,就是把地面給細分爲一個個網格,每個網格可以單獨的進行紋理貼圖,每次更換紋理的時候,也不會影響其他格子。

定義好一些常量,以便後面的講解。這些常量只需要先看一下,有個基本的印象即可,後面用到的時候,會具體解釋。

//地面的長度(x方向)
const GROUND_WIDTH = 64
//地面的寬度(z方向
const GROUND_HEIGHT = 64

//地面紋理的寬度 
const TEXTURE_WIDTH = 1024
//地面紋理的高度
const TEXTURE_HEIGHT = 1024

//一個方向上(s和t座標方向),把地面分爲多少個小塊
const GROUND_SUBDIVISION = 32

下面是具體的步驟。


1. 建立一塊平地

查閱 babylon.js 的相關文檔,直接調用api即可創建,代碼比較簡單,直接貼到下面:

const ground = MeshBuilder.CreateGround(
    name,
    { width: GROUND_WIDTH, height: GROUND_HEIGHT, subdivisions: GROUND_SUBDIVISION },
    scene,
)

上面代碼中,用到了三個在這一篇剛開始就已經定義好的常量 GROUND_WIDTH、GROUND_HEIGHT 和 GROUND_SUBDIVISION。前兩個常量分別代表的是要創建的地面的寬高,都是 64。它們所屬的座標系是裁剪座標系。由於 WebGL 中應用了很多座標系,可能有些同學還不是很瞭解,推薦去看看這篇文章WebGL座標系基礎。 至於 GROUND_SUBDIVISION 這個常量,指的是要把矩形的一條邊,分爲多少段,這一篇是把地面的 x、z 方向都分成了 32 段。

簡單的一行代碼,就可以創建出一塊平地了,看看效果:



2. 給“大地”貼上紋理

複雜的功能總是由一個個簡單的功能演變來的。首先先做最簡單的一步,先給我們剛剛創建好的這塊大地,貼上紋理,隨便找一張草地的圖片,查看babylon.js材質與紋理這一部分文檔,先給地面一個材質,然後在材質上進行貼圖,代碼如下:

//創建一個標準材質
const groundMaterial = new StandardMaterial('groundMaterial', scene);
//創建一個紋理
const groundTexture = new Texture('//storage.360buyimg.com/model-rendering-tool/files/jdLife/grass.jpg', scene)
//把紋理賦值給材質的漫反射紋理(這裏也可以是其他類型的紋理)
groundMaterial.diffuseTexture = groundTexture
//把材質賦值給地面的材質屬性
ground.material = groundMaterial

現在地面已經有了紋理貼圖了:



3. 分割地面爲一個個格子

在上一步,雖然已經實現了給地面貼紋理,但是效果肯定是不能滿足預定的需求的。如果對WebGL有相關了解的同學,應該會知道,如果要給材質的特定位置,貼上特定的紋理,就需要獲取材質上頂點數據,然後打上圖釘,再在紋理貼圖中,根據頂點對應的圖釘點,來獲取需要的圖片的位置。這應該是一步比較複雜的操作,而且 babylon.js 封裝的比較深,如果直接暴力的去實現起來,會是比較大的工程。

於是,本着 babylon.js 應該有封裝好的類,可以實現(或者經過簡單的改動後可以實現)這個需求的猜測,再次翻閱它的文檔,終於找到一個類似的例子。爲了方便閱讀,直接截取效果圖展示出來:

看了一下這個例子的代碼,可以總結爲:

  1. 使用了 babylon.js 的AdvancedDynamicTexture.CreateForMesh高級動態紋理爲地面創建紋理。
  2. 高級動態紋理提供了 addControl 方法,可以往紋理上添加各種“容器”。
  3. “容器”是 Babylon.js 的一個類 Container。
  4. “容器”也有 addControl 方法,可以在“容器”裏面繼續添加“容器”,即可以“套娃”。

babylon.js 的 AdvancedDynamicTexture 的實現原理,在這裏先不討論,但是現在有了上面這些知識點,結合 demo,就能對地面進行分格了。直接上代碼,把步驟寫在了註釋裏:

//首先調用AdvancedDynamicTexture的api,創建紋理
const groundTexture = GUI.AdvancedDynamicTexture.CreateForMesh(ground, TEXTURE_WIDTH, TEXTURE_HEIGHT, false)

//創建最外層的Container -- panel,它的寬高和紋理的一致
const panel = new GUI.StackPanel()
panel.height = TEXTURE_HEIGHT + 'px'
panel.width = TEXTURE_WIDTH + 'px'

//把panel添加到紋理上
groundTexture.addControl(panel)

//循環的建立一列列Row,並且加到panel上面
for (let i = 0; i < GROUND_SUBDIVISION; i++) {
  const row = new GUI.StackPanel()
  row.height = TEXTURE_HEIGHT / GROUND_SUBDIVISION + 'px'
  //把row添加到panel上
  panel.addControl(row)
  row.isVertical = false
  //在循環的,在每一行裏面建立一個個格子
  for (let j = 0; j < GROUND_SUBDIVISION; j++) {
    const block = new GUI.Rectangle()
    block.width = TEXTURE_WIDTH / GROUND_SUBDIVISION + 'px'
    row.addControl(block)     
  }
}

代碼裏用到了 TEXTURE_WIDTH 和 TEXTURE_HEIGHT 兩個常量,它們分別代表的是紋理的寬高。相對紋理的尺寸有更多瞭解的同學,可以參考下 WebGL紋理詳解之三:紋理尺寸與Mipmapping 這篇文章的解釋,這裏不展開細談。

看看這時候的效果:



4. 給每個格子單獨貼圖,並且存儲紋理 Image 對象

這裏的 Image 指的是 Babylon.js 裏面的一個類,爲了方便下文直接稱它 Image。
爲什麼要給每個格子單獨貼圖,這個無需解釋了。至於爲什麼要存儲每個格子的紋理Image對象,是爲了方便後面去修改貼圖。由於在創建這些格子的時候,是通過循環創建的,所以它們本身已經具備一定的順序了,因此只要把它們在創建的時候,都push到一個數組裏面(blockImageArray),讀的時候按照創建的順序傳入索引就可以了。

實現的時候,還是先實現最簡單的,讓每個格子的紋理都一樣就好了。在上一步的代碼基礎上添加,代碼如下:

...
  //在循環的,在每一行裏面建立一個個格子
  for (let j = 0; j < GROUND_SUBDIVISION; j++) {
    const block = new GUI.Rectangle()
    block.width = TEXTURE_WIDTH / GROUND_SUBDIVISION + 'px'
    row.addControl(block)
    //隱藏格子的邊框
    block.thickness = 0
    //創建Image對象
    const blockImage = new GUI.Image('blockTexture','//storage.360buyimg.com/model-rendering-tool/files/jdLife/grass.jpg')
    //把圖片添加到block上
    block.addControl(blockImage)
    //在外面的定義域裏面先創建好blockImageArray
    blockImageArray.push(blockImage)
  }

值得注意的是,上述代碼在創建 Image 對象的時候,是直接通過url進行動態導入的,這會造成,每次創建一個Image,就去發一個請求,顯然是存在性能問題的。
於是,再一次翻查babylon.js的文檔,尋求優化方案。Image 有一個 domImage 屬性,值的類型爲 HTMLImageElement,可以通過修改這個屬性,來修改圖片內容。所以只要事先加載圖片生成 HTMLImageElement 並且存儲在 imageSource ,在創建Image的時候,對它的 domImage 屬性進行賦值即可。優化後的代碼:

//把需要的圖片導入好,放到imageSource裏面
const imageSource : { [key: string]: HTMLImageElement } = { grass: img, stone: img }

...
//創建image的時候不再傳遞url參數了
const blockImage = new GUI.Image()
//對domImage屬性賦值
blockImage.domImage = imageSource.grass

現在來看一下效果:



5. 更改紋理

經過了上一步的操作,已經創建出來了一塊有模有樣的綠地了,接下來需要做的是紋理更換的功能。先實現個最簡單的:在最外層的 panel 監聽點擊事件,通過點擊的位置,判斷當前點擊的是地面的第幾行第幾列,然後找到 blockImageArray 對應的元素,對它的 domImage 進行再賦值就好了。代碼如下:

panel.onPointerClickObservable.add(e => {
    const { y, x } = e
    const perBlockWidth = TEXTURE_WIDTH / GROUND_SUBDIVISION
    const perBlockHeight = TEXTURE_HEIGHT / GROUND_SUBDIVISION
    const row = Math.floor(y / perBlockHeight)
    const col = Math.floor(x / perBlockWidth)
    const index = row * GROUND_SUBDIVISION + col
    blockObjArr[index].domImage = imageSource.stone
})

看看現在的效果:

到此爲止,就已經實現了創建地面並且可以改變紋理這個目標了。


實現之模型佔位計算篇

名詞:

  • 地面:案例中的平面
  • 模型:案例中需要計算佔位的物體
  • 索引編號:二維數組的下標
  • 網格座標系:將地面分割爲均等網格而形成的座標系
  • WebGL 座標系:原始 WebGL 座標系
  • 模型基點:模型原點
  • 轉換:從一個值換算到另一個值
  • 糾偏:把原有的座標數值加上偏正值(這裏是半個格子的長度或寬度)
  • 包圍盒:能把模型整個包起來的最小長方體 bounding

流程圖:https://www.processon.com/view/link/6238a14007912906f50e1ed7

經過上一篇的實現,現在已經創建了一塊地面,並將地面等分成了若干個網格。這時要獲取模型在地面上的佔位情況,就需要轉換爲獲取模型在地面上所佔格子的數據。下圖展示了,一個模型(房子)在地面上所佔的格子的情況(被佔的格子邊框顯示爲紅色):

爲了看起來直觀一些,我們將在下面的說明中把地面分割成 8*8 的網格體系。

以下是涉及到的常量:

//重新定義一下,讓地面分爲 8 * 8 的網格
const GROUND_SUBDIVISION = 8

//每一個格子在相機裁剪座標系中的寬度
const PER_BLOCK_VEC_X = GROUND_WIDTH / GROUND_SUBDIVISION
//每一個格子在相機裁剪座標系中的高度
const PER_BLOCK_VEC_Z = GROUND_HEIGHT / GROUND_SUBDIVISION

//模型位置向量在x軸方向的偏移量
const CENTER_OFF_X = PER_BLOCK_VEC_X / 2
//模型位置向量在z軸方向的偏移量
const CENTER_OFF_Z = PER_BLOCK_VEC_Z / 2

//半個格子在相機裁剪座標系中的寬度
const HALF_BLOCK_VEC_X = PER_BLOCK_VEC_X / 2
//半一個格子在相機裁剪座標系中的高度
const HALF_BLOCK_VEC_Z = PER_BLOCK_VEC_Z / 2

要確切地知道地面上的模型佔用了哪幾個格子,首先得建立地面網格座標系。還記得在上一篇中,生成這些格子的時候,是通過兩個 for 循環生成的嗎,其實在生成這些格子的時候,同時也產生了索引。爲了閱讀方便,我再貼一下生成網格的代碼:

for (let i = 0; i < GROUND_SUBDIVISION; i++) {
  const row = new GUI.StackPanel()
  row.height = TEXTURE_HEIGHT / GROUND_SUBDIVISION + 'px'
  //把row添加到panel上
  panel.addControl(row)
  row.isVertical = false
  //在循環的,在每一行裏面建立一個個格子
  for (let j = 0; j < GROUND_SUBDIVISION; j++) {
    const block = new GUI.Rectangle()
    block.width = TEXTURE_WIDTH / GROUND_SUBDIVISION + 'px'
    row.addControl(block)     
    //創建貼圖
    const blockImage = new GUI.Image()
    //對domImage屬性賦值
    blockImage.domImage = imageSource.grass
    block.addControl(blockImage)
    blockImageArray.push(blockImage)
  }
}

可以理解爲,每個網格的 i 和 j 就對應着它們在 z 方向和 x 方向的座標。

根據每個網格在網格座標系的 x 和 z 座標,設置索引編號(每個網格對應一個座標),索引編號的數據結構爲:

interface Coord {
  x: number,
  z: number
} 

放到 8*8 網格座標系中即爲:

上面這張圖是不是比較好理解了呢,看起來就像我們初中學習的平面直角座標系,原點在左上角,x 軸爲水平方向從左向右,z 軸是垂直方向從上到下。

對應到代碼中,我們可以通過創建一個二維數組,來存儲這個網格座標系。這樣就可以用地面網格的座標作爲索引,在二維數組中尋找對應的值,以判斷該網格上是否有模型佔位。


1. 創建網格座標系座標集合數組:groundStatus

網格座標系座標集合數組 groundStatus,我們把它定義爲一個 number 二維數組。

groundStatus 數據結構如下:

type GroundStatus = number[][]

二維數組中的每個元素與網格座標系中的座標一一對應。每個座標對應的初始值爲 0,代表當前座標沒有被佔位,當有模型放置在上面時,值 +1;當模型移開或刪除時,值 -1。不使用 boolean 作爲存儲類型的原因,是因爲 boolean 只有 true 和 false 兩種狀態,不能滿足更爲複雜的需求,比如在移動模型的時候,出現模型重疊的情況的時候,groundStatus 對應的格子上,就會有兩個模型。如果用 boolean 來表示的話,是沒辦法表示出來的,因爲它只有 true 和 false 兩種值。但是如果使用 number 的話,就可以在該格子對應的元素,把值修改爲 2,標識單前格子上有兩個模型佔位了。

在設計 groundStatus 索引時,以 x 還是 z 座標爲一維索引,在性能影響上區別不大。出於調試方面的考慮,建議以 z 座標爲一維索引,便於瀏覽器的控制檯二維數組的展示與網格座標系一一對應。



2. 模型基點向量與網格座標系座標的換算

模型基點向量指的是模型數據中的 position 屬性,定義了模型在 WebGL 座標系中的位置。position 是一個三維向量,遵守的是 WebGL 座標系,比如當 position 值爲 (0, 0, 0) 的時候,出現在地面的位置就是地面的中心點。爲了方便,下文我都會把它叫做,模型的基點

圖片來源:在InfraWorks中編輯3D模型的基點或插入點

WebGL 座標系的 (0, 0, 0) 換算成地面座標系,就是網格座標 (3, 3)、(3, 4)、(4, 3)、(4, 4) 這四個格子的交點。如下圖所示:

黃色的方塊,代表圓點在地面上所佔的格子。實際佔位只有1格的模型,在這個位置向上取整的時候,最小的佔位也是4個格子,一旦涉及到碰撞檢測等功能,會出現模型佔位過大的問題。所以我們需要對中心點進行偏移,使模型在網格座標系中的佔位儘量接近模型真實佔位,往右下角——x、z 各偏移半個網格的單位即可,這時候 (0, 0, 0) 對應的基點座標就是 (4, 4) 格子的中心點了。偏移的原則是保證模型的基點能落在網格座標系的某個格子的中點,以便更爲準確地進行模型佔位的計算。如下圖:

這裏值得注意的是,當我們傳入模型的位置向量爲 (x, y, z) 的時候,我們會手動的把模型的位置改爲 (x + CENTER_OFF_X, 0, z - CENTER_OFF_Z)(Y 軸向量本次不涉及計算,因此可以省略)。z 向量的計算爲減法,因爲 WebGL 座標系的 z 軸向上爲正的,而網格座標系 z 軸向上爲負。

這裏我們封裝一個傳入模型的位置向量、返回該點的地面座標的函數:

function getGroundCoordByModelPos(buildPosition: Vector3): Coord {
  const { _x, _z } = buildPosition
  const coordX = Math.floor(GROUND_WIDTH / 2 / PER_BLOCK_VEC_X + (_x + CENTER_OFF_X) / PER_BLOCK_VEC_X)
  const coordZ = Math.floor((GROUND_HEIGHT / 2 - (_z - CENTER_OFF_Z)) / PER_BLOCK_VEC_Z)
  return { x: coordX, y: coordZ }
}


3. 獲取模型佔位區在 WebGL 座標系的關鍵數據

這一步是爲了獲取模型的實際佔位相關數據,爲後續的網格座標系佔位轉換做準備。

模型存在最小包圍盒的概念,也叫最小邊界框,用於界定模型的幾何圖元。包圍盒/邊界框可以是矩形,也可以是更爲複雜的形狀,爲方便描述,我們這裏採用矩形包圍盒/邊界框的方式進行說明。下文中簡稱包圍盒。

圖片來源:3D 碰撞檢測

當我們將 WebGL 座標系的模型投射到網格座標系上時,可以得到一片區域:

黃色區域代表的是模型佔位區域,黑色點則是模型的基點。babylon.js 提供了相關的 api,可以計算出模型包圍盒的邊界與基點的距離,這裏的值均基於 WebGL 座標系。

我們將這些距離存儲到 rawOffsetMap 的對象中,數據結構如下:

interface RawOffsetMap {
  rawOffsetTop: number
  rawOffsetBottom: number
  rawOffsetLeft: number
  rawOffsetRight: number
}

計算代碼如下:

/*
 @param { AbstractMesh[] } meshes 模型導入後返回的結果
 @param { Vector3 } scale 模型的縮放倍數
*/
function getRawOffsetMap(meshes: AbstractMesh[], scale: Vector3 = new Vector3(1, 1, 1)): RawOffsetMap {
  //聲明最小的向量
  let min = null
  //聲明最大的向量
  let max = null
  
  //對模型的meshes數組進行遍歷
  meshes.forEach(function (mesh) {
    //babylon.js 提供的api,可以遍歷該mesh的和mesh的所有子mesh,找到它們的邊界
    const boundingBox = mesh.getHierarchyBoundingVectors()

    //如果當前的最小向量不存在,那麼把當前的mesh的boundingBox的min屬性賦值給它
    if (min === null) {
      min = new Vector3()
      min.copyFrom(boundingBox.min)
    }

    //如果當前的最大向量不存在,那麼把當前的mesh的boundingBox的max屬性賦值給它
    if (max === null) {
      max = new Vector3()
      max.copyFrom(boundingBox.max)
    }

    //對最小向量和當前的boundingBox的min屬性,從x,y,z這三個分量進行比較與再賦值
    min.x = boundingBox.min.x < min.x ? boundingBox.min.x : min.x
    min.y = boundingBox.min.y < min.y ? boundingBox.min.y : min.y
    min.z = boundingBox.min.z < min.z ? boundingBox.min.z : min.z

    //對最大向量和當前的boundingBox的max屬性,從x,y,z這三個分量進行比較與再賦值
    max.x = boundingBox.max.x > max.x ? boundingBox.max.x : max.x
    max.y = boundingBox.max.y > max.y ? boundingBox.max.y : max.y
    max.z = boundingBox.max.z > max.z ? boundingBox.max.z : max.z
  })

  return {
    rawOffsetRight: max.x * scale.x,
    rawOffsetLeft: Math.abs(min.x * scale.x),
    rawOffsetBottom: max.z * scale.z,
    rawOffsetTop: Math.abs(min.z * scale.z)
  }
}


4. 獲取模型佔位區在網格座標系上的關鍵數據:offsetMap

這一步是將模型 WebGL 座標系的佔位關鍵數據轉換爲網格座標系中的數據。

如上圖所示,黃色的格子,代表的是模型基點所在的格子。紅色是模型在網格座標系轉化之後的佔位——當模型邊界佔位不滿一格的時候(比如只佔了格子的一半),按佔滿一格來算。這四個數據,我們使用 offsetMap 對象來存儲:

interface OffsetMap {
  offsetLeft: number,
  offsetRight: number,
  offsetTop: number,
  offsetBottom: number
} 

在上一節中,已經計算出模型的 rawOffsetTop,rawOffsetBottom,rawOffsetLeft,rawOffsetRight。現在只要把這幾個關鍵值一一轉化爲 offsetMap 對應的關鍵值即可。

上圖中黃色區域是模型在 WebGL 座標系中的佔位,紅色區域是將模型佔位向上取整後,在網格座標系中所佔網格的集合。rawOffsetMap 與 offsetMap 中字段的轉化關係爲:rawOffsetLeft 對應 offsetLeft;rawOffsetRight 對應 offsetRight;rawOffsetTop 對應 offsetTop;rawOffsetBottom 對應 offsetBottom。以 rawOffsetLeft 轉化爲 offsetLeft 爲例,將 rawOffsetLeft 減去半個格子的寬度(HALF_BLOCK_VEC_X),然後除以一個格子的寬度(PER_BLOCK_VEC_X),再向上取整。下面爲具體代碼:

function getModelOffsetMap(rawOffsetMap: RawOffsetMap): OffsetMap {
  const { rawOffsetMapLeft, rawOffsetRight, rawOffsetBottom, rawOffsetTop } = rawOffsetMap
  const offsetLeft = Math.ceil((rawOffsetLeft - HALF_BLOCK_VEC_X) / PER_BLOCK_VEC_X)
  const offsetRight = Math.ceil((rawOffsetRight - HALF_BLOCK_VEC_X) / PER_BLOCK_VEC_X)
  const offsetTop = Math.ceil((rawOffsetTop - HALF_BLOCK_VEC_Z) / PER_BLOCK_VEC_Z)
  const offsetBottom = Math.ceil((rawOffsetBottom - HALF_BLOCK_VEC_Z) / PER_BLOCK_VEC_Z)
  return {
    offsetBottom,
    offsetLeft,
    offsetRight,
    offsetTop
  }
}


5. 計算出模型在網格座標系的包圍盒索引:bounding

這一步我們將計算出模型包圍盒在 groundStatus 中的索引下標,以便通過 groundStatus 來判斷對應網格是否已被佔位。bounding 即爲佔位模型在 groundStatus 數據中的幾個邊界索引值。

bounding 的數據結構如下:

interface Bounding {
  minX: number,
  maxX: number,
  minZ: number,
  maxZ: number
} 

還是先通過一張圖,解釋一下 bounding 對象中的四個值指的什麼:

上圖中,紅色區域是模型在網格座標系中所佔網格。bounding 數據中的四個值,代表了模型包圍盒邊界網格在 groundStatus 中的索引數組下標,作爲更新 groundStatus 中的佔位數值的依據。

基於第4步中得到的 offsetMap 數據,結合第2步中的基點座標,即可算出最終的 bounding:

function getModelBounding(buildPosition: Vector3, offsetMap: OffsetMap): IBounding {
  const modelGroundPosCoord = getGroundCoordByModelPos(buildPosition)
  const { x, y } = modelGroundPosCoord
  const { offsetBottom, offsetLeft, offsetRight, offsetTop } = offsetMap
  
  const minX = x - offLeft
  const maxX = x + offRight
  const minZ = y - offTop
  const maxZ = y + offBottom
  
  return {
    minX,
    maxX,
    minZ,
    maxZ
  }
}

至此,關於模型的 bounding 的計算就完成了。

6. 更新佔位數據

在上一步,已經獲取到了模型在地面座標系的 bounding,這時候只需利用bounding的值,對 groundstatus 進行賦值就好了,代碼如下:

//索引邊界判斷
function isValidIndex(x: number, z: number): boolean {
  if (x >= 0 && x < GROUND_SUBDIVISION && z >= 0 && z < GROUND_SUBDIVISION) return true
  return false
}

function setModlePosition(groundStatus: GroundStatus, bounding: Bounding) {
  const { minX, maxX, minZ, maxZ } = bounding

  for (let i = minZ; i <= maxZ; i++) {
    for (let j = minX; j <= maxX; j++) {
      if (isValidIndex(j, i))
        groundStatus[i][j]++
    }
  }
}

後續的待優化項

該項目的地面時一塊平地,沒有考慮深度方面的信息。如果是在地面有起伏的場景下,現在的數據結構是不足以應付的。如果是那種階梯式高度的場景(地面由n片高度不同的平地構成),那麼至要把 groundStatus 數組的元素的數據結構進行改造,加入地面高度標識的屬性即可以滿足需求。但是如果是那種高低起伏並且帶有坡度的地形,那麼很難進行改造。


成品展示


參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章