2018-08-19

【進度】Cyber Sprite外語版 (3)

謎題到底怎麼解呢?


外語版技術問題還沒完……,在Linux上測試之後,發現還能寫第三篇進度文。
在Linux寫程式有時候像在玩解謎遊戲,常遇到莫名其妙的問題。

同時發在巴哈姆特



本遊戲Option裡可以選擇語言,但是玩家第一次開遊戲的時候,要設成什麼語言呢?

Cyber Sprite開發期就有預留延伸空間,程式都寫好了只是先不放進正式版裡。
當時做成第一次玩會進入這個選單,第二次以後就跳過。


但是,真的開始做外語版後想改成另一種方式。
上面的方法有兩個缺點,一是要把所有語言的字型都載入,不能只載入需要的,會浪費一些空間。二是如果以後增加其他語言,畫面上東西會太多。(雖然不確定還會不會增加語言)

改成先偵測作業系統的語言設定,決定最初使用的語言,然後顯示這一行訊息。


偵測作業系統語言的方法,Windows使用GetUserDefaultUILanguage()和LCIDToLocaleName()。
LANGID langID = GetUserDefaultUILanguage();
int wcharLen = LCIDToLocaleName(langID, NULL,0,0); //取得需要的長度
WCHAR* wcharBuffer = _alloca(sizeof(WCHAR)*wcharLen);
LCIDToLocaleName(langID, wcharBuffer,wcharLen,0); //把ID轉換成名稱

//UTF16轉UTF8,buffer為char* buffer
for(int i=0;i<wcharLen;i++){
  buffer[i] = wcharBuffer[i];
}
使用前幾篇「記憶體配置技巧」裡說的,用_alloca()在stack上配置陣列。

Linux是取得LANG這個環境變數
char* langEnv=getenv("LANG");



目前為止大部分在Windows作業,接下來在Linux測試看看。

(先用網路線把電子妖精送到Linux開發機,然後編譯Linux版)
好了,艾莉兒,執行遊戲。

艾莉兒:載入普通字型,然後載入偽斜體字型……
艾莉兒:畫字完成。


艾莉兒:奇怪,所有字都變偽斜體了。


用printf()檢查一下,字型index沒填錯,fontconfig的pattern字串也對,到底是為什麼?
又是解謎遊戲了,艾莉兒、鈷寶,開始抽絲剝繭,找出真相吧。


…………(兩小時後)


發現cairo一個讓人很無言的現象。(第一篇說過,這次把畫字函式庫從freetype換成cairo)

詳細情況:
在cairo載入字型需要以下步驟
1.用cairo_ft_font_face_create_for_ft_face()建立一個cairo_font_face_t物件。
2.如果是粗體或偽斜體,呼叫cairo_ft_font_face_set_synthesize()設定。
3.用cairo_scaled_font_create()建立一個cairo_scaled_font_t物件。
4.使用這個cairo_scaled_font_t來畫字。

參考:cairo說明書的這兩頁
FreeType fonts
cairo_scaled_font_t

其中藏了這兩個秘密
a.建立cairo_font_face_t時會檢查現有物件,如果有屬性相同的就不產生新的,傳回同一個物件。
b.cairo_font_face_t實際被使用前可以更改synthesize屬性,但第4步用它畫字一次後就不可更改。

說明書也沒有寫,是我做了多次實驗才發現。
艾莉兒:不知道cairo內部是怎樣,我想1~3只是設flag之類的,畫字時才真正產生內部資源吧。

光這樣還看不出問題在哪,不過來看看實際發生什麼事。
如果先建普通字型,後建偽斜體字型


所以之後所有字都變成偽斜體了。
由於synthesize屬性不可更改,也不能用「畫字→修改屬性→畫另一種字」的做法。

鈷寶:……。(搔頭)
艾莉兒:怪問題……。

的確是難懂又難搞的問題。
想避開這個問題必須先建偽斜體,後建普通字型


艾莉兒:煩吔,哪有載入字型還要求順序的,而且還是隱藏陷阱。

如果我的引擎也這樣設計未免太奇怪,所以設法在引擎裡動些手腳,讓艾莉兒載入字型時不用照特定順序。

--------

解釋一下上面提到的偽斜體是什麼,它跟斜體不一樣。
偽斜體(oblique):只是把普通字型變斜

斜體(italic):字的形狀也要改變

斜體必須在字型檔裡儲存另一個形狀,並不是每個字型都有支援,通常是襯線體、小寫字母才有,在顯示文字的軟體裡如果要求italic但字型檔裡沒有斜體時,有時會用偽斜體代替。

去查CSS的font-style也可以看到normal、oblique、italic三種。不過在幾個瀏覽器試一下,Edge、Opera、Firefox、Konqueror都沒有正確支援oblique,設定「font-style:oblique」時還是顯示italic。
DirectWrite倒是可以正確畫出兩者,所以上面的字是叫艾莉兒畫的(自己寫程式用DirectWrite畫)。



這次把很多二代新做的東西移植到一代,把遊戲整個跑一輪看有沒有問題,玩到第一關boss前面時……

艾莉兒:報告,發生Segmentation fault,程式強制結束。
Windows版不會發生這種事,是哪裡出問題呢?
艾莉兒:主人,用那個方法,map檔和EIP。

如果查得到當掉當時EIP暫存器的值,可以用它來debug。Windows可以執行eventvwr開啟事件檢視器查到。而Linux記得以前的發行版會在命令列顯示一些資訊,但不知從哪一版開始改成只顯示一行Segmentation fault,不知道該怎麼查,上網查了一下才知道可以用dmesg指令。

嗯……,查到是在libgdk_pixbuf內部當掉,讀圖檔的函式庫,大概知道是哪一行了,加兩個printf看看。
printf("A\n");
int imageID=loadImage("boss1.png");
printf("B\n");
結果命令列只印出A就當掉。很好,一擊中的。
艾莉兒:確定是coroutine的問題了。

這是在coroutine裡讀圖檔,第一篇說過以前用libpng和libjpeg,這次換成gdk-pixbuf,消耗的記憶體比較多,以前配置的stack空間就不夠用。
把stack大小改成256KB(256×1024),總算不會當了。

只有Windows原生支援coroutine,Linux必須用組合語言刻一個,這方法很冒險,二代我決定不再使用。但是一代已經寫好的程式要全部改掉有困難(因為coroutine太好用了,到處都在用),只能把stack調大一點應付,反正先把這一作應付過去,二代再改做法。



試玩途中,用手把玩一段時間後……,突然螢幕保護程式起動,畫面被蓋住。
艾莉兒:主人,快動滑鼠!
往往就在這個時候自機中彈,因此玩的時候得每隔一段時間動一下滑鼠避免這種事。

因此發現還要做一個東西:將螢幕保謢程式和自動休眠暫停,因為只有鍵盤和滑鼠輸入被作業系統判定為有在操作電腦,而手把輸入不會。

在Linux怎麼做?看起來有點難。
艾莉兒:鈷寶,我們查查看。
……
鈷寶:好像是……D-Bus。

得去學D-Bus怎麼用了,參考以下資料:
GIO說明書
GIO說明書,GDBusConnection的頁面
SDL程式碼,下載後看其中的SDL_dbus.c
org.freedesktop.ScreenSaver說明
步驟大致如下(省略錯誤檢查的code)
//有數種函式庫可用,這裡用GIO
#define _GLIB_TEST_OVERFLOW_FALLBACK
#include<gio/gio.h>

//需要的字串
const char BUS_NAME[]="org.freedesktop.ScreenSaver";
const char OBJECT_PATH[]="/org/freedesktop/ScreenSaver";
const char INTERFACE_NAME[]="org.freedesktop.ScreenSaver";
const char INHIBIT_METHOD[]="Inhibit";

//建立一個連結物件
GDBusConnection* dbusConnection = g_bus_get_sync(
  G_BUS_TYPE_SESSION,NULL,NULL);
GError* error=NULL;
GVariant* parameters = g_variant_new(
  "(ss)","CyberSpriteEngine","test this feature");
//呼叫method
GVariant* retValue=g_dbus_connection_call_sync(gp->dbusConnection,
  BUS_NAME,OBJECT_PATH,INTERFACE_NAME,INHIBIT_METHOD,
  parameters,NULL,G_DBUS_CALL_FLAGS_NONE,-1,NULL,&error
);
g_variant_unref(retValue);


//遊戲結束時關閉連結,就會恢復螢幕保護程式
g_dbus_connection_close_sync(dbusConnection,NULL,NULL);
Windows的話,在Windows 10手把輸入也被判定為有在操作,所以不用特別處理。
但是記得Windows 7手把輸入不能暫停螢幕保護程式,還是加一下比較保險。
方法比Linux簡單很多,一個函式就解決了。
SetThreadExecutionState(ES_CONTINUOUS|ES_DISPLAY_REQUIRED);

(試玩的情況)
啊……,不小心按到道具鈕,浪費一個道具。
危險!快用道具……,可惡,按鈕不靈。
艾莉兒:撐下去啊,撐完一輪就行了!
鈷寶:主人……加油。
手把已經有些按鈕損壞(因為我常玩ACT和STG,手把汰換率很高),加上螢幕保護程式的妨礙,其實是在不利的條件玩,最後總算咬著牙通過Hard。
可以玩完一輪沒出錯誤,不過還要修改一些地方。例如vertex shader在日文版裡本來用英文寫,跟譯者討論之後決定改成日文「頂点シェーダー」,但有些台詞忘了改。(遊戲裡的確有一關以shader為題材)



最後針對各個字型各別調整。同樣是無襯線體各個字型還是有微妙的差異,先把遊戲用到的所有字型印出來,用肉眼確認。
字是從角色介紹裡取一些字。

此圖不是遊戲本體,是另外寫個程式畫字。
然後決定哪個語言用粗體,哪個語言用普通。

艾莉兒:這個我們幫不上忙,主人只能自己來了。

大致看來微軟正黑體比較細,明瞭體粗體特別粗。所以Windows中文用粗體,其他中、日文情況用普通,英文則是覺得粗體比較適合本作的風格。
即使英文字型同樣設成大小20,DejaVu Sans的字比較大,為了防止空間不夠的問題,把Linux版的字大小改成19。

有另一個方法能調整粗度:使用畫邊框的功能並讓邊框和字相同顏色。這方法比較靈活,可畫出任意粗細,而不像粗體只有on、off兩個選項。

(此圖用Inkscape畫)
DirectWrite和cairo都可以畫字的邊框,但要幫艾莉兒追加這個功能有些費工,這次先不做。

這次調整字型的工作要由人類確認,不知道能不能叫電子妖精自動調整。先畫一個「一」字,然後檢查點陣圖(讀取pixel各的值)求出筆劃寬度,再計算要加寬多少,理論上是做得到的。

另外找到一篇不錯的資料,介紹Windows Phone內建的字型,桌機版Windows也可以參考。
MSDN ドキュメントに見るフォントの話



這次改版除了增加語言以外,也把一些二代新做的東西移植到一代。
  • 更換畫字和讀圖檔的函式庫,因此把系統需求提升為Windows 7和Ubuntu 16.04。
  • 碰撞判定演算法用SSE改寫。
  • 把OpenAL的部分改良,減少播音效的延遲。
  • 雙螢幕時在兩個螢幕都可以開全螢幕模式。
有的部分決定維持原樣,不用新方法。
  • 繪圖部分的plane(2D圖像物件)種類,這個工比較大,所以不更動。
  • OpenGL版本維持2.0,沒換成D3D11和OpenGL 3.3。
  • 顏色byte順序是RGBA(R在最低byte,A在最高),二代改成BGRA。
    看起來是小事但一代做到後半時覺得是一大錯誤。以前因為OpenGL預設是RGBA就用RGBA,但大部分繪圖軟體用的是BGRA,不能把16進位數值從繪圖軟體複製到企劃資料增加了很多困擾。但修正這個要改很多地方,不想在外語版花太多時間所以決定放著。
  • 音檔解碼函式庫也沒有換新。
    二代Linux版本來想用gstreamer,但它有很多問題所以想放棄,打算找時間試試其他函式庫。
還有一些問題是現在才發現,要移植到二代。
  • 英文不能在單字中間換行。
  • 改良LibreOffice外掛。
  • 偵測系統語言。
  • cairo讀字型的問題。
  • 暫停螢幕保護程式。



關於插圖:
在這張圖上花了好幾天。
再度改良透視計算技術,有精密的計算才能畫出這種歪斜的角度。

左邊格透視計算,字是標示是何種消失點。


草圖

畫到後面發現Z軸消失點應該右移一點,讓X軸消失點在左邊才比較好看,不過這是畫了也沒酬勞的圖,這張就不管了,下一張再改進。

右邊格透視計算


草圖

完成圖裡地面的格子是正方形,邊長是透視計算算出來的。
也開發出計算影子長度的方法,草圖裡紫色四邊形是shadow volume。根據計算桌子的影子會完全被桌子擋住,所以沒畫桌子的影子。
但是人物沒有照正確計算畫所以有點奇怪,艾莉兒身體左半邊應該會小一點。

也有試一下用3D軟體建模輔助。

有研究出FOV該設成什麼值,預設的視角太寬不適合用在畫圖。
不過此法缺點是不易調整,例如某個地方太空曠時,很難推算如何修改模型才能減少空曠的感覺,用我自己的公式算反而比較好用。

沒有留言 :

張貼留言