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.mzph.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…

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 …

互联网医院开发:引领智慧医疗新时代

随着科技的迅猛发展和互联网的普及&#xff0c;传统医疗模式正在迎来一场深刻的变革。互联网医院的崛起&#xff0c;打破了时间和空间的限制&#xff0c;为患者和医疗机构带来了更加便捷、高效、安全的医疗服务体验。本文将从技术角度深入探讨互联网医院的开发&#xff0c;包括…

多线程(八)

一、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;某车载监控设备…

ACM实训第十七天

Is It A Tree? 问题 考试时应该做不出来&#xff0c;果断放弃 树是一种众所周知的数据结构&#xff0c;它要么是空的(null, void, nothing)&#xff0c;要么是一个或的集合满足以下属性的节点之间有向边连接的节点较多。 •只有一个节点&#xff0c;称为根节点&#xff0c;它…

【Crypto】摩丝

文章目录 一、摩斯解题感悟 一、摩斯 很明显莫尔斯密码 iloveyou还挺浪漫 小小flag&#xff0c;拿下 解题感悟 莫尔斯密码这种题还是比较明显的

智能锁千千万,谁是你的NO.1,亲身实测凯迪仕传奇大师K70旗舰新品

智能锁千千万&#xff0c;谁是你的NO.1。欢迎来到智哪儿评测室&#xff0c;这次我们为大家带来了凯迪仕传奇大师K70系列的一款重磅新品。 在科技的浪潮中&#xff0c;家居安全领域正经历着前所未有的变革。智能锁越来越成为家的安全守护神&#xff0c;以及智能生活的得力助手。…

Android 11 Framework实时监听Activity堆栈变化

核心类 Framework中有一个类SystemActivityMonitoringService专门用于监控Activity堆栈变化&#xff0c;属于隐藏Api&#xff0c;应用侧无法调用。此类位于 packages/services/Car/service/src/com/android/car/SystemActivityMonitoringService.java 方法 void registerTa…

大数据Hive中的UDF:自定义数据处理的利器(下)

在上一篇文章中&#xff0c;我们对第一种用户定义函数&#xff08;UDF&#xff09;进行了基础介绍。接下来&#xff0c;本文将带您深入了解剩余的两种UDF函数类型。 文章目录 1. UDAF1.1 简单UDAF1.2 通用UDAF 2. UDTF3. 总结 1. UDAF 1.1 简单UDAF 第一种方式是 Simple(简单…

每日一题《leetcode--382.链表随机结点》

https://leetcode.cn/problems/linked-list-random-node/ 这道题我们首先看到题目中的要求&#xff1a;在单链表中随机选取一个链表中的结点&#xff0c;要使每个结点被选取的概率是一样的。 当我们看到随机这两个字时&#xff0c;应该就会想起rand()这个函数。接着我们把使用这…

自己搭建内网穿透

本文介绍使用最新版frp搭建内网穿透&#xff0c;最新版本的frp在配置上与之前有很大不同&#xff0c;需要使用.toml文件进行配置。其中主要问题出现在toml文件内部。 一、云服务器配置 下载frp sudo apt update sudo apt install wget wget https://github.com/fatedier/frp…