Raylib 繪制自定義字體是真的難搞。我的需求是程序可以加載多種自定義字體,英文中文的都有。
我調試了很久成功了!
很有用的參考,建議先看一遍:
瞿華:raylib繪制中文內容
個人筆記|Raylib 的字體使用 - bilibili
再放一下第一篇文章的可用示例代碼:
#include <raylib.h>int main() {InitWindow(800,600,"世界你好");Image img=GenImageColor(800,600,WHITE);//讀取字體文件unsigned int fileSize;unsigned char *fontFileData = LoadFileData("c:\\windows\\fonts\\simhei.ttf", &fileSize);//ImageDrawCircleEx(&img, 400,300,200,10,BLACK);SetTraceLogLevel(LOG_WARNING);SetTargetFPS(120);while (!WindowShouldClose()) {//將要輸出的內容放到字符串中(必須是utf8編碼)char text[]="世界,你好!";// 將字符串中的字符逐一轉換成Unicode碼點,得到碼點表int codepointsCount;int *codepoints=LoadCodepoints(text,&codepointsCount);// 讀取僅碼點表中各字符的字體Font font = LoadFontFromMemory(".ttf",fontFileData,fileSize,32,codepoints,codepointsCount);// 釋放碼點表UnloadCodepoints(codepoints);BeginDrawing();ClearBackground(WHITE);DrawTextEx(font,text,(Vector2){50,50},32,5,RED);EndDrawing();//釋放字體UnloadFont(font);}UnloadImage(img);//釋放字體文件內容UnloadFileData(fontFileData);return 0;
}
(是的,圖片img好像沒有用)
關鍵步驟概括為:
- LoadFileData 讀取字體文件
- (while 主循環)
- 準備好要輸出的文本
- LoadCodepoints 用準備的文本加載碼點
- LoadFontFromMemory 得到含需要輸出的文本的字符的字體
- UnloadCodepoints 卸載碼點
- BeginDrawing 開始繪制 使用剛剛的字體繪制文本
- EndDrawing 結束繪制
- UnloadFont
- (循環結束)
- UnloadFileData 卸載字體文件
注意每一輪循環都用指定文本的碼點加載了新的字體,繪制好后才卸載該字體。
我試圖將這一系列操作封裝成函數DrawTextPlus,發現UnloadFont必須要在EndDrawing后面執行,不然會輸出失敗。
下面這張圖更離譜了,大錯特錯!!
但是如果在一幀內調用多次BeginDrawing和EndDrawing,還是會出事。。出事代碼如下
還是錯誤的代碼,別復制
void DrawTextPlus(const string& s, int x, int y, int fs = 32)
{BeginDrawing();// 將字符串中的字符逐一轉換成Unicode碼點,得到碼點表int codepointsCount;int *codepoints=LoadCodepoints(s.c_str(),&codepointsCount);// 讀取僅碼點表中各字符的字體Font font = LoadFontFromMemory(".ttf", fontFileData, fileSize, 32, codepoints, codepointsCount);// 釋放碼點表UnloadCodepoints(codepoints);DrawTextEx(font,s.c_str(),(Vector2){x,y},fs,0,RED);EndDrawing();//釋放字體UnloadFont(font);
}
出現了閃爍現象:
所以一幀還只能調用一次BeginDrawing,EndDrawing。那只能采取其他措施了。
如果真的不封裝,需要在同一幀輸出不同文本的話,以下代碼可以正常運行:
#include <raylib.h>
#include <string>int main() {InitWindow(800,600,"世界你好");//讀取字體文件unsigned int fileSize;unsigned char *fontFileData = LoadFileData("c:\\windows\\fonts\\simhei.ttf", &fileSize);SetTraceLogLevel(LOG_WARNING);SetTargetFPS(120);//將要輸出的內容放到字符串中(必須是utf8編碼)ssize_t scnt = 4;const std::string strings[] {"DarkVoxel", "Battle of Phantom", "Poemaze", "TerraSurvivor"};while (!WindowShouldClose()) {std::string total_texts{""};for (size_t i {0}; i < scnt; ++i)total_texts += strings[i];// 將字符串中的字符逐一轉換成Unicode碼點,得到碼點表int codepointsCount;int *codepoints=LoadCodepoints(total_texts.c_str(),&codepointsCount);// 讀取僅碼點表中各字符的字體Font font = LoadFontFromMemory(".ttf",fontFileData,fileSize,32,codepoints,codepointsCount);// 釋放碼點表UnloadCodepoints(codepoints);BeginDrawing();ClearBackground(WHITE);//可以按需要輸出了,只要total_texts中有該字符就可以正常輸出for (size_t i {0}; i < scnt; ++i)DrawTextEx(font,strings[i].c_str(),Vector2{50.0f, 50.0f * i}, 32.0f, 5.0f, RED);EndDrawing();//釋放字體UnloadFont(font);}//釋放字體文件內容UnloadFileData(fontFileData);return 0;
}
可以發現有好幾個地方值得注意以及一點想法:
1.字體整個文件的讀取還是在循環前(也就是在程序的載入階段可以一口氣把所有的字體文件讀完放進一個容器中)
2.需要輸出的文本得提前準備好(如果真的在項目中這樣,未免太難受了)
3.在準備碼點的時候,可以把需要輸出的文本合并在一起(當然可以進行一個字符去重以提高效率)
4.繪制文本的時候只要字符在合并好的文本之中,就可以正常輸出
5.每幀都進行了加載和卸載字體的操作(還是變慢了)
6.最后程序退出前卸載時要釋放所有的字體文件內容。(釋放容器)
小項目就上面這樣的寫法應該可以接受。但是中大項目就不一樣了,動不動就要輸出一大堆文本,不可能搞一堆string存在那里,看的都煩;而且每幀都要重新準備字體效率低下。
經過進一步思考,我形成了另一種思路。我在上面的代碼中添加了一些【偽代碼】:
#include <raylib.h>
#include <string>
#include <map>
#include <vector>【
容器,存儲所有詞匯std::string LSTR(const std::string輸出內容ID)
{//在我的項目中,支持多語言,我弄一個CSV,專門存儲每種語言的詞匯,//那么這個輸出內容ID就可以是中文,方便我閱讀代碼。返回真實的輸出內容
}
】int main()
{InitWindow(800,600,"世界你不好");//讀取讀取你的CSV文件并存儲到一個容器中,以供上面的LSTR函數使用map<std::string, pair <std::string, unsigned char*>> 所有需要用到的字體名稱以及路徑、數據;{{..., {..., nullptr}},{..., {..., nullptr}},};】【for (const auto& fdt : ...){unsigned int fileSize;unsigned char *fontFileData = LoadFileData(字體文件路徑, &fileSize);把fontFileData存進去string 整合串= 去重后的把CSV文件所有內容拼接在一起的字符串;// 將字符串中的字符逐一轉換成Unicode碼點,得到碼點表int codepointsCount;int *codepoints=LoadCodepoints(整合串.c_str(), &codepointsCount);// 讀取僅碼點表中各字符的字體Font font = LoadFontFromMemory(取字體路徑擴展名, 字體文件內容fontFileData,fileSize, 200, codepoints, codepointsCount);把字體裝進去// 釋放碼點表UnloadCodepoints(codepoints);}】SetTraceLogLevel(LOG_WARNING);SetTargetFPS(120);//將要輸出的內容放到字符串中(必須是utf8編碼)while (!WindowShouldClose()) {BeginDrawing();ClearBackground(WHITE);//可以按需要輸出了,只要total_texts中有該字符就可以正常輸出
//CUR_FONT 是一個宏,獲取當前字體DrawTextEx(CUR_FONT,LSTR("CSV中"),Vector2{50.0f, 50.0f}, 80.0f, 5.0f, BLACK);DrawTextEx(CUR_FONT,LSTR("包含的內容"),Vector2{50.0f, 130.0f}, 80.0f, 5.0f, BLACK);DrawTextEx(CUR_FONT,LSTR("都可以寫"),Vector2{50.0f, 210.0f}, 80.0f, 5.0f, BLACK);EndDrawing();}【for (auto& fdt : ...){UnloadFileData(字體文件內容指針);}】return 0;
}
注意你需要準備一個文件(例如CSV格式的),每行存儲一個你需要的字符串,然后LSTR函數的參數就是你訪問任意一個字符串的索引(可以是數字【我覺得挺煩的,還要查】,可以是字符串【本身】)。正如我注釋中寫的,我的程序支持多語言,因此可以每行一個中文,逗號,一個英文,然后用中文索引,特別方便。
這樣的結構雖然很難搞,但是大大簡化了中間繪制文本的代碼,只需要加個LSTR這樣的函數即可,無需手動準備一堆string來搞臨時的字體再輸出。
如果你不想撰寫新的文件存儲所要用的字符串,還有幾種偷懶的方法(僅供參考):
(1)寫一個輔助的程序,在要編譯前運行它,提取你的源文件中的字符串然后整合在一起,再把字符串寫進去然后編譯(霧)。
(2)把所有字符(例如漢字)加載進去(日常試試可以,實際運用肯定不現實,內存都要爆了)
上面的偽代碼可能看的不是很明白,我也不可能全部幫你補全,只能提供一些我跑成功的項目的代碼或是截圖,希望對你有幫助:
語言、詞匯處理
enum LangID
{Chinese = 0,English = 1,
};
#define LANG_CNT 2//下標宏
#define LID_LANG 0 //各語言名稱
#define LID_GAME_NAME 1vector<vector<string>> lang_words;bool ReadLanguage();constexpr const char* PunctAndNumberString(void)
{return "0123456789,.?/<>()~`[]{}\\|\"\':;!@#$%^&*-=_+ ";
}
constexpr const char* PunctAndNumberStringIncludingChinese(void)
{return " 0123456789,.?/<>()~`[]{}\\|\"\':;!@#$%^&*-=_+,。?!、()【】“”‘’;:《》·…—";
}
string ObtainNormalEnglish(const string& s)
{string res;bool wordbeg{ true };for (char ch : s){if (wordbeg && isalpha(ch)){res += islower(ch) ? toupper(ch) : ch;wordbeg = false;}else if (isalpha(ch)){res += ch;}else if (ch == '_' || ch == ' '){res += ' ';wordbeg = true;}}return res;
}
string AssembleTotalChineseString(void);
string AssembleTotalEnglishString(void)
{string res;for (char ch = 'A'; ch <= 'Z'; ++ch)res += str(ch);for (char ch = 'a'; ch <= 'z'; ++ch)res += str(ch);res += PunctAndNumberString();return res;
}
string UniqueChinese(const string& s) {string result;unordered_set<int> chineseChars;for (size_t i = 0; i < s.length(); i++) {// 檢查當前字符是否是中文字符if ((s[i] & 0xE0) == 0xE0) {int codePoint = ((s[i] & 0x0F) << 12) | ((s[i + 1] & 0x3F) << 6) | (s[i + 2] & 0x3F);// 如果當前中文字符不在哈希集合中,則將其添加到結果字符串和哈希集合中if (chineseChars.find(codePoint) == chineseChars.end()) {result += s.substr(i, 3);chineseChars.insert(codePoint);}// 由于中文字符占用3個字節,因此增加索引i的值i += 2;}else {result += s[i];}}return result;
}bool ReadLanguage()
{string path = g.data_dir + "Language.csv";if (!ExistFile(path)){ErrorLogTip(nullptr, "Cannot Find the Language File :(\n" + path, "ReadLanguage");return false;}DebugLog("讀取語言...");vector<string> lines = ReadFileLines(path);int i{ 1 };while (i < lines.size()){string line = lines.at(i);if (line.empty()){++i;continue;}line = strrpc(line, " ", "$");line = strrpc(line, ",", " ");stringstream ss;string tmp;ss << line;vector<string> langs;for (int i = 0; i < LANG_CNT; ++i){ss >> tmp;tmp = strrpc(tmp, "$", " ");tmp = strrpc(tmp, "^", ",");langs.push_back(tmp);}// DebugLog(str(langs));lang_words.push_back(langs);++i;}for (const auto& idt : itemdata)lang_words.push_back(vector{ idt.cn_name, ObtainNormalEnglish(idt.en_name) });for (const auto& edt : entitydata)lang_words.push_back(vector{ edt.cn_name, ObtainNormalEnglish(edt.en_name) });for (const auto& bdt : buffdata)lang_words.push_back(vector{ bdt.cn_name, ObtainNormalEnglish(bdt.en_name) });for (const auto& pdt : placeabledata)lang_words.push_back(vector{ pdt.cn_name, ObtainNormalEnglish(pdt.en_name) });for (const auto& rdt : random_tips)lang_words.push_back(rdt.versions);DebugLog("共計", lang_words.size(), "個詞匯,支持", LANG_CNT, "門語言");return true;
}
string AssembleTotalChineseString(void)
{string res;//英文也要for (char ch = 'A'; ch <= 'Z'; ++ch)res += str(ch);for (char ch = 'a'; ch <= 'z'; ++ch)res += str(ch);//然后是中文for (const auto& pr : lang_words)res += pr.at(Chinese);res += PunctAndNumberStringIncludingChinese();res = UniqueChinese(res);return res;
}
#define CHN_FONTNAME "Sthginkra Italic"map<LangID, string> lang_font_names
{{Chinese, CHN_FONTNAME},{English, "Andy Bold"},
};#define CUR_FONTNAME (g.lang_font_names[g.lang].c_str())
#define CENTER_TITLE_CHN_FONTNAME "釘釘進步體"map<string, pair<string, LangID>> used_fonts
{{"Andy Bold", {"ANDYB.TTF", English}},{CHN_FONTNAME, {"ZhouFangRiMingTiXieTi-2.otf", Chinese}}, //我不是舟批{CENTER_TITLE_CHN_FONTNAME, {"DingTalk JinBuTi.ttf", Chinese}},
};DebugLog("安裝", used_fonts.size() - 1, "個字體...");unsigned char* pFileData{ nullptr };auto iter = used_fonts.begin();
for (;iter != used_fonts.end(); ++iter)
{if (iter->second.second == English)continue;auto pr = make_pair(iter->first,make_pair(ProduceMemoryFont(iter->second.first, iter->second.second, &pFileData),pFileData)); //見下文cout << iter->second.first << " " << iter->second.second << " " << pr.first << " " << pr.second.first.glyphCount << '\n';g.fonts.insert(pr);
}DebugLog("加載 " + str(g.fonts.size()) + " 個字體完畢");
Font ProduceMemoryFont(const string& filename, LangID lid, unsigned char** pFileData)
{string s;switch (lid){case Chinese:s = AssembleTotalChineseString();break;case English:s = AssembleTotalEnglishString();break;default:return GetFontDefault();}Font font;unsigned int fileSize{ 0U };unsigned char* fontFileData = LoadFileData((g.font_dir + filename).c_str(), &fileSize);*pFileData = fontFileData;if (fontFileData == nullptr){DebugLog("ERROR: fontFileData is empty");}int codepointsCount;cout << "LoadCodepoints...\n";cout << "s=" << s << '\n';int* codepoints = LoadCodepoints(s.c_str(), &codepointsCount);if (!codepoints){cout << "ERROR: LoadCodePoints failed\n";}cout << "CodepointsCount=" << codepointsCount << '\n';cout << "FileSize=" << fileSize << '\n';string ext = GetFileExtension(filename.c_str());cout << "Ext=" << ext << '\n';// 讀取僅碼點表中各字符的字體cout << "LoadFontFromMemory...\n";font = LoadFontFromMemory(ext.c_str(), fontFileData,fileSize, 200, codepoints, codepointsCount); //200挺合適的// 釋放碼點表cout << "UnloadCodepoints...\n";UnloadCodepoints(codepoints);return font;
}
DebugLog("卸載", used_fonts.size(), "個字體...");
for (const auto& fn : used_fonts)
{UnloadFont(g.fonts[fn.first].first);UnloadFileData(g.fonts[fn.first].second);
}
控制臺輸出截圖(非中文字符去重我好像沒做):
怎么樣,有思路了嗎?
大概就是把要輸出的字符串提前收集好,然后裝載字體一次就行,后面就隨心所欲輸出就行了。
還有幾點:
1.裝載字體時的字號選 200 是挺合適的值,如果太低就馬賽克了,太高會出問題
2.CSV文件可能是這樣的: