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编码)s
size_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 1
vector<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文件可能是这样的: