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编码)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);
}

控制台输出截图(非中文字符去重我好像没做):
L

怎么样,有思路了吗?
大概就是把要输出的字符串提前收集好,然后装载字体一次就行,后面就随心所欲输出就行了。

还有几点:
1.装载字体时的字号选 200 是挺合适的值,如果太低就马赛克了,太高会出问题
2.CSV文件可能是这样的:
软件名称:Modern CSV

  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Android绘制自定义地图可以通过以下步骤实现: 1. 获取地图数据:首先,需要获取自定义地图的数据,这可以是由地图提供者提供的矢量数据、栅格数据或者是在地图编辑器中创建的自定义地图图层。 2. 解析地图数据:将获取到的地图数据解析成可以在Android绘制的格式。这可能涉及到解析矢量数据、转换图像数据格式或者解析自定义地图文件格式。 3. 创建自定义绘制视图:在Android中,我们可以使用自定义绘制视图来实现绘制自定义地图。通过继承View类或者SurfaceView类,并重写相应绘制方法(如onDraw方法),可以在画布上绘制地图数据。 4. 绘制地图数据:在自定义绘制视图中,通过使用Canvas对象和Paint工具等工具,可以将解析好的地图数据绘制到画布上。根据需求,可以自定义地图的样式、颜色、线条粗细等。 5. 处理地图交互:在绘制地图的同时,还可以处理一些地图交互操作,例如实现点击地图位置获取地理坐标、实现地图缩放和平移等操作。可以通过添加触摸事件监听器来实现相应的交互处理。 6. 更新地图数据:如果自定义地图需要动态更新,可以根据需要实现数据更新的逻辑,例如定期从服务器获取最新地图数据,并重新解析和绘制地图。 7. 优化性能:在绘制自定义地图时,需要考虑性能优化,避免频繁的绘制操作导致卡顿。可以使用缓存技术、合并绘制操作、使用硬件加速等手段来提高绘制效率。 总之,Android绘制自定义地图的关键是获取并解析地图数据,然后通过自定义绘制视图来展示地图数据,并处理相应的地图交互操作。 ### 回答2: Android绘制自定义地图可以通过以下步骤实现: 1. 准备地图数据:首先需要获取自定义地图数据,可以是地图的矢量数据或者图片,如SVG格式的矢量地图或者PNG格式的图片地图。可以从地图提供商获取或者自行制作。 2. 绘制地图背景:在Android的绘图环境Canvas上创建一个画布,并且使用地图背景色填充整个画布。 3. 坐标转换:根据地图数据和设备屏幕的尺寸,将坐标进行转换,使得地图上的坐标点对应到屏幕上的实际像素点。 4. 绘制地图要素:根据地图数据,将地图要素逐个绘制到画布上,如绘制地图的道路、河流、建筑等等。可以借助Android的Canvas来完成这些绘制操作,如使用画笔设置颜色、线宽等属性,使用路径(Path)来绘制直线、曲线,使用矩形(Rect)来绘制建筑等等。 5. 绘制标注和图标:根据需要,在地图上绘制标注和图标,如标记地点的名称、行政区划边界、POI点等等。可以使用Canvas的文本绘制功能来绘制标注的文本,可以使用Bitmap来绘制图标。 6. 交互操作:可以通过触摸事件来实现对地图的交互操作,如放大、缩小、平移地图等。可以根据用户的操作,重新计算地图的显示范围并更新画布上的绘制内容。 7. 地图样式和主题:可以根据需求自定义地图的样式和主题,如修改地图要素的颜色、线宽、字体等属性,调整地图的显示效果。 绘制自定义地图需要有一定的绘图技巧和地理知识,同时需要理解Android的绘图原理和API。通过上述步骤,可以实现在Android应用中绘制自定义的地图,满足特定需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值