Raylib 繪制自定義字體的一種套路

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好像沒有用)

關鍵步驟概括為:

  1. LoadFileData 讀取字體文件
  2. (while 主循環)
  3. 準備好要輸出的文本
  4. LoadCodepoints 用準備的文本加載碼點
  5. LoadFontFromMemory 得到含需要輸出的文本的字符的字體
  6. UnloadCodepoints 卸載碼點
  7. BeginDrawing 開始繪制 使用剛剛的字體繪制文本
  8. EndDrawing 結束繪制
  9. UnloadFont
  10. (循環結束)
  11. 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);
}

invoke

出現了閃爍現象:
flash

所以一幀還只能調用一次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);
}

控制臺輸出截圖(非中文字符去重我好像沒做):
L

怎么樣,有思路了嗎?
大概就是把要輸出的字符串提前收集好,然后裝載字體一次就行,后面就隨心所欲輸出就行了。

還有幾點:
1.裝載字體時的字號選 200 是挺合適的值,如果太低就馬賽克了,太高會出問題
2.CSV文件可能是這樣的:
軟件名稱:Modern CSV

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/14484.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/14484.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/14484.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

W801 實現獲取天氣情況

看了小安派&#xff08;AiPi-Eyes 天氣站&#xff09;的源碼&#xff0c;感覺用W801也可以實現。 一、部分源碼 main.c #include "wm_include.h" #include "Lcd_Driver.h"void UserMain(void) {printf("\n user task \n");Lcd_Init();Lcd_Clea…

MySQL主從復制(五):讀寫分離

一主多從架構主要應用場景&#xff1a;讀寫分離。讀寫分離的主要目標是分攤主庫的壓力。 讀寫分離架構 讀寫分離架構一 架構一結構圖&#xff1a; 這種結構模式下&#xff0c;一般會把數據庫的連接信息放在客戶端的連接層&#xff0c;由客戶端主動做負載均衡。也就是說由客戶…

RabbitMQ 消息隊列安裝及入門

市面常見消息隊列中間件對比 技術名稱吞吐量 /IO/并發時效性&#xff08;類似延遲&#xff09;消息到達時間可用性可靠性優勢應用場景activemq萬級高高高簡單易學中小型企業、項目rabbitmq萬級極高&#xff08;微秒&#xff09;高極高生態好&#xff08;基本什么語言都支持&am…

為什么MySQL推薦使用utf8mb4代替utf8?

前言 在MySQL數據庫的世界里&#xff0c;字符集的選擇直接影響著數據的存儲和檢索方式&#xff0c;尤其是對于多語言支持至關重要的應用而言。近年來&#xff0c;utf8mb4字符集逐漸成為MySQL中存儲Unicode字符的標準選擇&#xff0c;逐步取代了傳統的utf8字符集。本文將詳細探…

leetcode124 二叉樹中的最大路徑和-dp

題目 二叉樹中的 路徑 被定義為一條節點序列&#xff0c;序列中每對相鄰節點之間都存在一條邊。同一個節點在一條路徑序列中 至多出現一次 。該路徑 至少包含一個 節點&#xff0c;且不一定經過根節點。 路徑和 是路徑中各節點值的總和。 給你一個二叉樹的根節點 root &…

【Crypto】Rabbit

文章目錄 一、Rabbit解題感悟 一、Rabbit 題目提示很明顯是Rabbit加密&#xff0c;直接解 小小flag&#xff0c;拿下&#xff01; 解題感悟 提示的太明顯了

redis核心面試題二(實戰優化)

文章目錄 10. redis配置mysql實戰優化[重要]11. redis之緩存擊穿、緩存穿透、緩存雪崩12. redis實現分布式session 10. redis配置mysql實戰優化[重要] // 最初實現OverrideTransactionalpublic Product createProduct(Product product) {productRepo.saveAndFlush(product);je…

MQTT 5.0 報文解析 05:DISCONNECT

歡迎閱讀 MQTT 5.0 報文系列 的第五篇文章。在上一篇中&#xff0c;我們已經介紹了 MQTT 5.0 的 PINGREQ 和 PINGRESP 報文。現在&#xff0c;我們將介紹下一個控制報文&#xff1a;DISCONNECT。 在 MQTT 中&#xff0c;客戶端和服務端可以在斷開網絡連接前向對端發送一個 DIS…

手把手教你搭建一個花店小程序商城

如果你是一位花店店主&#xff0c;想要為你的生意搭建一個精美的小程序商城&#xff0c;以下是你將遵循的五個步驟。 步驟1&#xff1a;登錄喬拓云平臺進入后臺 首先&#xff0c;你需要登錄喬拓云平臺的后臺管理頁面。你可以在電腦或移動設備上的瀏覽器中輸入喬拓云的官方網站…

2024.5.26 機器學習周報

目錄 引言 Abstract 文獻閱讀 1、題目 2、引言 3、創新點 4、Motivation 5、naive Lite-HRNet 6、Lite-HRNet 7、實驗 深度學習 解讀SAM(Segment Anything Model) 1、SAM Task 2、SAM Model 2.1、Patch Embedding 2.2、Positiona Embedding 2.3、Transformer …

移動端適配:vw適配方案

vw (Viewport Width) 是一種長度單位&#xff0c;代表視口寬度的百分比。1vw 等于視口寬度的1%。在網頁設計和前端開發中&#xff0c;vw 單位常用于實現響應式設計和屏幕適配&#xff0c;尤其是針對不同尺寸和分辨率的移動設備。 為什么使用vw適配&#xff1f; 響應式: 使用v…

互聯網醫院開發:引領智慧醫療新時代

隨著科技的迅猛發展和互聯網的普及&#xff0c;傳統醫療模式正在迎來一場深刻的變革。互聯網醫院的崛起&#xff0c;打破了時間和空間的限制&#xff0c;為患者和醫療機構帶來了更加便捷、高效、安全的醫療服務體驗。本文將從技術角度深入探討互聯網醫院的開發&#xff0c;包括…

【openpcdet中yaml文件的DATA_AUGMENTOR學習】

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄 前言一、代碼二、詳細解釋DISABLE_AUG_LISTAUG_CONFIG_LIST1. gt_sampling2. random_world_flip3. random_world_rotation4. random_world_scaling 總結 前言 提示…

多線程(八)

一、wait和notify 等待 通知 機制 和join的用途類似,多個線程之間隨機調度,引入 wait notify 就是為了能夠從應用層面上,干預到多個不同線程代碼的執行順序.( 這里說的干預,不是影響系統的線程調度策略 內核里的線程調度,仍然是無序的. 相當于是在應用程序…

Pod容器資源限制和探針

目錄 一、資源限制 1.Pod和容器的資源請求和限制 2.CPU 資源單位 案例一 案例二 二、健康檢查&#xff0c;又稱為探針&#xff08;Probe&#xff09; 1.探針的三種規則 2.Probe支持三種檢查方法 3.探測獲得的三種結果 案例一&#xff1a;exec 案例二&#xff1a;htt…

OneMO同行 心級服務:中移物聯OneMO模組助力客戶終端寒冷環境下的穩定運行

中移物聯OneMO模組以客戶為中心&#xff0c;基于中國移動心級服務要求&#xff0c;開展“OneMO同行 心級服務 標定一流”高標服務主題活動&#xff0c;升級“服務內容““服務方式”和“服務意識”&#xff0c;為行業客戶提供全新的服務體驗。 近日&#xff0c;某車載監控設備…

Hive語法學習總結

Hive SQL語法學習總結 hive參數庫操作1.創建庫2.具體案例3.庫的其他操作 表和庫的路徑演示表的操作創建表插入數據 hive參數 一 hive常用交互命令hive -e sql語句hive -f sql文件 //文件中是sql語句二 參數的設置方式一&#xff1a;在客戶端中設置參數(當次有效)set 參數名參…

ACM實訓第十七天

Is It A Tree? 問題 考試時應該做不出來&#xff0c;果斷放棄 樹是一種眾所周知的數據結構&#xff0c;它要么是空的(null, void, nothing)&#xff0c;要么是一個或的集合滿足以下屬性的節點之間有向邊連接的節點較多。 ?只有一個節點&#xff0c;稱為根節點&#xff0c;它…

【Crypto】摩絲

文章目錄 一、摩斯解題感悟 一、摩斯 很明顯莫爾斯密碼 iloveyou還挺浪漫 小小flag&#xff0c;拿下 解題感悟 莫爾斯密碼這種題還是比較明顯的

【董曉算法】競賽常用知識之圖論3(最近公共祖先)

前言&#xff1a; 本系列是學習了董曉老師所講的知識點做的筆記 董曉算法的個人空間-董曉算法個人主頁-嗶哩嗶哩視頻 (bilibili.com) 動態規劃系列&#xff08;還沒學完&#xff09; 【董曉算法】動態規劃之線性DP問題-CSDN博客 【董曉算法】動態規劃之背包DP問題&#xff…