簡單粗暴的地圖生成

這是我大概幾個星期前隨便寫的小玩意,看到一票人都在玩這個,就自己也整了個來玩。

使用的技術

  • Kotlin 原因:我只會這個

寫了這個之後我才發現 Kotlin 的這個語法真的是超級適合寫這種小玩意。

工作流程

  1. 隨機生成點
  2. 擴大 (doublify)
  3. 一定概率下兩兩相連,擴大連線,形成主大陸
  4. 勻一下 (averagify)
  5. 再隨機生成點
  6. 再勻一下 (averagify8)
  7. 擴大 (triplify)
  8. 根據等高線畫出地形圖,特定高度隨機生成裸露岩石灰色
  9. 生成幾個點,根據水往低處流的特點用 A* 走一遍生成河流
  10. 在特定高度隨機生成黃點,可以看作是南瓜或者城市
  11. 在海邊走一遍 bfs 生成大小有限的沙灘 (這部分耗時比較嚴重,可我真的不知道怎麼優化那個 bfs 了)

是不是很簡單呢。比 Nova 的那個簡單多了。這個只適合生成一個孤立的小島, Nova 那個是一個大陸啊。而且自然得多。

效果

演化史

從最原始的版本越來越新。

一開始啥都沒有,畫質也很低,連線是 100%。

連線改成概率。

增加沙灘,這時沙灘不是 bfs 出來的,是等高線的一部分。這張生成的很猙獰,我很喜歡,不過這時代碼的 averagify 邏輯有點問題。

這時這個 bug 很明顯。

bug 修復。改變配色方案(之前是隨手寫的顏色代碼,後來改成了 flutter 裡面拿出來的 Material Design 的顏色)。

改進顏色層次。

改變地圖大小,趨近於現在的大小,增加主大陸外的小島。

沒變。

增加每次評估四個點的 A* 算出來的河流。

這個也比較好玩,剛好有一大段卡進沙灘的範圍。河流的 A* 改成評估八個點。

改變了一些參數。其實沒咋變,這張生成的比較特異而已。

增加河流流到內陸中央盆地會生成小湖,只是這時寫的超傻逼。另外河流數量過剩了。

改進前面說的小湖,能看了。河流數量似乎還是那樣。

減少河流。

進一步減少河流。現在稍微好點了。取消基於等高線的沙灘。

沙灘改為 bfs 。

增加[城市/南瓜]。

主要邏輯部分

  • 源碼在 GitHub ice1000/map-gen

fun main(vararg args: String) { /// the first map val map1 = gameMapOf(60, 60) val ls = map1.genRandPts(11) /// initial points map1 { ls.forEach { (x, y, i) -> val v = rand(200) + 1000 + i * (MAGIC_NUM_1 / ls.size) Pair(x, y).pnd.forEach { p -> set(p.first, p.second, v + rand(100) - 50) p.pnd.forEach { (x, y) -> set(x, y, v + rand(100) - 50) } } set(x, y, v) } } ls eachTwo { (x1, y1, _), (x2, y2, _) -> if (rand(5) >= 2) { map1 { val k = (map1[x1, y1] + map1[x2, y2]) shr 1 Line(Point(x1, y1), Point(x2, y2)).allPoints.forEach { (x, y) -> 70 % { map1[x, y] = k + rand(200) - 100 } Point(x, y).pnd.forEach { 70 % { map1[it] = k + rand(200) - 100 } } } } } } /// random points repeat(100) { val x = rand(map1.width - 2) + 1 val y = rand(map1.height - 2) + 1 map1[x, y] = rand(300) + 300 } repeat(3) { map1.averagify() } /// expand the size, the second map val map2 = map1.doublify() /// traverse and add random points map2.traverse { (x, y, i) -> map2[x, y] = rand(200) - 50 + i } /// random points, as islands val ls2 = map2.genRandPts(9) map2 { ls2.forEach { (x, y, i) -> val v = rand(800) + 200 + i * (MAGIC_NUM_1 / ls2.size) Pair(x, y).pnd.forEach { p -> set(p.first, p.second, v + rand(100) - 50) p.pnd.forEach { (x, y) -> set(x, y, v + rand(100) - 50) } } set(x, y, v) } } /// expand the map size, the final map map2.averagify8().averagify() val map3 = map2.triplify() map3.averagify8().averagify().averagify() /// now the map is ready /// rivers(based on A* algorithm) repeat(rand(4, 6)) { map3.rivers.add(map3.genRiver()) } map3.generateImage(args.getOrElse(0, { "out.png" }))}

這樣主大陸就好了。這部分代碼所用到的框架我覺得可以提煉出來作為一個圖形庫,嘿嘿。

繪製部分

這部分負責繪製,以及走 A* 形成河流,還有黃色點點。

fun GameMap.generateImage(fileName: String) { image(width, height) { traverse { (x, y, i) -> color(x, y, when (i) { in -10000..300 -> DEEP_BLUE in 0..500 -> BLUE in 0..900 -> SHALLOW_BLUE in 0..1200 -> MIDDLE_GREEN in 0..1400 -> L_LIGHT_GREEN in 0..1600 -> LIGHT_GREEN in 0..1700 -> DARK_GREEN in 0..1900 -> M_DARK_GREEN in 0..2070 -> if (1 == rand(72)) GRAY else BROWN else -> if (1 == rand(24)) GRAY else WHITE }) } val rg = 801..960 fun bfs(p: Point, block: (Point) -> List<Point>) { val q: Queue<Point> = LinkedList<Point>() q.offer(p) var i = 0 while (q.isNotEmpty() && ++i < 50000) { if (contains(q.peek()) && colorOf(q.peek()) != SAND && get(q.peek()) in rg) { block(q.peek()).filter { it !in q && get(it) in rg }.forEach { q.offer(it) color(it, SAND) } } q.poll() } } val i = rand(10, width - 10) var j = 0 while (j < height && get(i, j) !in rg) ++j bfs(Point(i, j), { it.pndL }) bfs(Point(i, j), { it.pndR }) /// pumpkins repeat(6) { genRandPtSatisfying { get(it) in 1151..1450 }.pnd9.forEach { color(it, ORANGE) } } rivers.forEach { it.flatMap { it.pnd5 }.forEach { color(it, SHALLOW_BLUE) } } write(fileName) }}

推薦閱讀:

做白馬王子,有錢就夠了嗎?
【戰報】絕地求生香蕉計劃邀請賽次日戰報:FTD戰隊霸權統治、強勢登頂
3D遊戲的先行者 《古惑狼》是如何誕生的
智商不足要充值?這幾個辦法可能對你有點用!
學院派體驗之——中華電子遊戲研究協會年會之旅

TAG:绘图 | 游戏 | 编程 |