LV. 27
GP 3k

【閒聊】自訂HUD系統

樓主 merakx
GP9 BP-
本來是想在一年前的舊文章上補充,但寫到最後根本幾乎整篇都是新的,就另外發了。
HUD是指head-up display,在遊戲中代表固定在畫面中,不隨人物動作而改變位置的介面,像是物品欄、bossbar、計分板sidebar都算是HUD。
適用於單人模式,改造成多人模式通用也是可以,但是過程有億點麻煩,這篇略過。

前提知識:
延伸閱讀:

先上測試影片:

【Action Bar】
在物品欄上面一點點的地方有一個文字顯示區,稱為action bar,在原版遊戲中用來顯示部分提示文字,例如「附近有怪物遊蕩,無法休息」、「正在播放:C418 - 11」。
用指令自訂的方法是/title <玩家> actionbar <raw JSON text>
action bar有一個很重要的特性:不能換行

在action bar中,文字是置中顯示的。
置中顯示就像這樣,文字在畫面的正中間
如果需要讓文字偏離中線,有一種感覺很蠢但是有用的方法,就是用空白排版。
像是這樣,這行字稍微往左邊歪了     
     或是這樣,這行字稍微往右邊歪了

【置中顯示的原理】
想像有一個記事本的游標,一開始什麼文字都沒有的時候,定義游標的位置在0px(px:像素)。
之後輸入一段寬度總共是120px的文字,所以現在游標的位置在120px。置中顯示會拿游標位置的1/2處對齊畫面中央,在這個例子中是60px的位置。
也就是說,置中對齊實際上就只是一種「0px會左右亂跑的向左對齊」而已,文字是最左方在0px,而不是中央在0px。

如果我在文字後面多塞60px的空白,游標位置會跑到180px,置中的對齊線就是90px,雖然還是1/2的游標位置,但是對於看得到的文字部分,感覺像是對齊在3/4,也就是前面提到的空白排版。
接下來,假設我能做出一種東西,叫做「寬度是負60px的空白」,然後用這個東西取代剛才的60px空白,游標位置就會回到60px。
如果我再多打一點字上去,應該可以預期有一些內容「覆蓋」在一開始的文字上面。

有一種特殊的情況,如果我無論輸入多寬的文字,都用對應的負寬度空白把游標推回0px。
這個時候會有一個很奇特的現象:我不管多輸入幾組「文字+負寬度空白」,都不會影響原有文字的顯示位置,因為游標位置會一直被推回0px,置中對齊線也就被固定在0px。
所以我現在可以做出糊成一團的文字,刪掉任何一組都不會讓其他組的位置移動。

但是全部疊在一起顯然沒什麼意義,只會讓人難以閱讀。
現在再拿空白排版出來用一次,在「文字+負寬度空白」前後塞進一對定位用空白,這對空白的寬度相加等於0px,變成「定位用空白+文字+負寬度空白+定位用空白」。
這樣做的效果是:把游標移動到指定位置→顯示一段文字→把文字占用的寬度吃掉把游標推回0px
於是糊成一團的文字分開了,而且依然保留了「刪掉任何一組都不會讓其他組的位置移動」的特性。

前面提到過,置中對齊只是一種「0px會左右亂跑的向左對齊」,既然我把0px固定在畫面中央了,意思就是現在action bar被我改造成向左對齊的模式。

【定寬空白】
Minecraft字體的定義方式是透過一個JSON檔案,決定每個字對應到哪張圖片材質、圖片材質要怎麼顯示。
所以定寬空白的製造方式就是指定一張透明的圖片,把顯示寬度設定成固定的大小。
實際上比這麻煩一點點啦,那個參數叫做height,是設定圖片的顯示高度而不是寬度,長寬會被等比縮放,但對於正方形的圖片而言,寬度=高度就是了。

覺得自製各種寬度的空白很麻煩嗎?
網路上有已經做好的資源包,可以直接去抓來用。

簡易使用說明:
在raw JSON text裡面塞{"translate": "space.N", "font": "space:default"},就可以顯示寬度N的空白,N從-1024到1024都有,另外還有幾個1024的倍數,但那些數字通常會跑到螢幕外面,很少用到。

【吃掉文字占用的寬度】
這就沒有什麼捷徑了,自製一份字體,把每一個字元都對應成一個適當的負寬度空白。
在raw JSON text裡要做的事情就是把一樣的內容用兩種字體各顯示一次。
例如[{"score": {"name": "@s", "objective": "hp"}, "font": "test:normal"}, {"score": {"name": "@s", "objective": "hp"}, "font": "test:reverse"}]

【製造換行效果】
前面說過,action bar不能換行,所以要用一些特殊的手段製造多行文字的視覺效果。
方法是自製字體,調整ascent參數,讓文字向下移動。ascent的意思是「上升量」,所以向下移動要輸入負數。
只向下移動的理由是,ascent不能大於height,但是下限在非常遠的地方。

回憶一下文章開頭的測試影片。
右下方第一行字(顯示gametime)是拿內建材質把ascent調成-45的結果,這個字體我取名成line1。
第二行字(顯示攻擊力)則是把ascent設成-54,這個字體我取名成line2。
沒錯,每一行字都要新增一種字體。
眼尖的話會發現物品欄3和8有特效,標記「選取的時候會降低攻擊+凋零」的位置,這也是用自製自體做出來的。
那個弱化系統有單獨測試影片,連結放在這邊,有興趣可以點去看。

【獨立運作的HUD系統】
上面那部影片裡面自製了三個HUD元素,其中攻擊力是常駐顯示,時間會在非慣用手拿時鐘的時候顯示,物品欄標記位置可能會改變(參見弱化系統的測試影片)。
猜猜看這幾個東西是怎麼加在一起的?

簡單但搞笑的做法是寫一堆條件測試,標記位置的動畫總共有16格,時間有顯示和不顯示共2種狀態,攻擊力有整數、小數後一位、小數後兩位共3種狀態,寫16*2*3=96行不同的/execute if if if判斷就搞定了,讚啦。
或是用動態-靜態物件轉換那篇提過的字串連接,看起來簡潔一點,但是要新增或刪除HUD內容的時候,很有可能要去改循環執行/title指令的那個function檔案。

我這次是用一個list儲存各個HUD內容,每個內容包括用於識別的ID和實際的顯示內容,看起來像這樣:
{elements: [{id: "hotbar_mark", text: <raw JSON text>}, {id: "clock", text: <raw JSON text>}, {id: "atk", text: <raw JSON text>}]}
循環執行的/title只要負責顯示{"nbt": "elements[].text", "interpret": true, "separator": ""}就可以了,elements裡面裝了什麼和這個function無關。
剩下的部分就各自為政,像是時鐘的循環function是
execute if ... run data modify storage test:hud elements[{id: "clock"}].text set value '{"translate": "space.120", "font": "space:default", "extra": [...]}'
execute unless ... run data remove storage test:hud elements[{id: "clock"}]

做成這樣的好處是,之後不管我要新增什麼東西上去,都不用動到以前寫的指令,在新的function裡面修改elements就能把文字放上HUD。
而且因為有ID,所以不需要寫一堆指令去找出應該把文字放在哪裡,只要用elements[{id: "..."}].text就可以了。


製作一個容易擴充的HUD系統需要知道的東西大概就這樣。
還原楓之谷那篇是自訂字體的一個經典例子,有時間的話推薦過去看看。
9
-
未登入的勇者,要加入討論嗎?
板務人員:

face基於日前微軟官方表示 Internet Explorer 不再支援新的網路標準,可能無法使用新的應用程式來呈現網站內容,在瀏覽器支援度及網站安全性的雙重考量下,為了讓巴友們有更好的使用體驗,巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業系統版本才可使用)

face我們了解您不想看到廣告的心情⋯ 若您願意支持巴哈姆特永續經營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學】