文章目录
一、前言
嗨,大家好,我是新发。
我在GitHub
上看到了一个名字叫BullshitGenerator
(狗屁不通文章生成器)的项目,地址:https://github.com/menzi11/BullshitGenerator
有接近1.5k
的星星,我下载下来玩了一下,有点意思,不过它是使用Python
和JavaScript
写的,嘛,那我来做一个Unity
版的吧,顺便把文字生成长图的功能也做一下~
二、流程概览
首先,我们要测试一下BullshitGenerator
的功能,看它做了什么;
接着,查看BullshitGenerator
的源码,看它是怎么做的;
最后,我们使用Unity
来实现,并进行拓展,比如生成文字长图。
三、BullshitGenerator项目测试
1、项目下载
BullshitGenerator
项目地址:https://github.com/menzi11/BullshitGenerator
下载下来后,可以看到它提供了两个版本的实现:Python
和JavaScript
,
2、测试JavaScript版
我们先来玩一下JavaScript
版的,使用浏览器打开index.html
,
在主题输入框中输入文章主题,点击生成按钮,它就可以生成一篇狗屁不通的文章,
3、测试Python版
现在我们来玩下python
版的,使用python
执行这个自动狗屁不通文章生成器.py
,
输入文章主题,按回车,即可生成一篇狗屁不通的文章,
四、BullshitGenerator源码分析
1、JavaScript版源码分析
我们使用文本编辑器打开index.html
,
1.1、定义变量(配置表)
它首先定义了几个列表,其实就是配置表,
注:作者使用
中文
做为变量名
和函数名
,一开始看有点不大习惯~
let 论述 = [
"现在,解决主题的问题,是非常非常重要的。 所以, ",
...
]
let 名人名言 = [
"伏尔泰曾经说过,不经巨大的困难,不会有伟大的事业。这不禁令我深思",
...
]
let 后面垫话 = [
"这不禁令我深思。 ",
...
]
let 前面垫话 = [
"曾经说过",
...
]
接着封装一些必要的函数,我来一一讲解一下~
1.2、随便取一句
使用一个Math.random
函数生成一个0~1
的随机数,乘以列表长度,再使用Math.floor
函数向下取整,得到一个列表的随机索引,最后根据索引从列表中取值。
function 随便取一句(列表){
let 坐标 = Math.floor( Math.random() * 列表.length );
return 列表[坐标];
}
1.3、随便取一个数
从最小值和最大值之间随机取一个数,这个是用来做概率随机的,
function 随便取一个数(最小值 = 0,最大值 = 100){
let 数字 = Math.random()*( 最大值 - 最小值 ) + 最小值;
return 数字;
}
1.4、来点名人名言
先从名人名言
列表中随机取一句,然后把这句话中的关键字“曾经说过”
替换为前面垫话
列表的随机一句,再把关键字“这不禁令我深思”
替换为后面垫话
列表的随机一句话,
function 来点名人名言(){
let 名言 = 随便取一句(名人名言)
名言 = 名言.replace("曾经说过", 随便取一句(前面垫话) )
名言 = 名言.replace("这不禁令我深思", 随便取一句(后面垫话) )
return 名言
}
画成图:
1.5、来点论述
从论述
列表中随机取一句,把关键字"主题"
替换为我们的主题变量
,注意这里用到了正则表达式,RegExp("主题", "g")
是一个正则表达式,g
表示全局匹配,即把句子中所有的关键字"主题"
都进行匹配,比如我们取到一句论述:"主题的发生,到底需要如何做到,不主题的发生,又会如何产生。 "
,这句论述中是有两个"主题"
关键字的,我们将其替换为我们的主题变量
,假设我们的主题变量
为:"一天掉多少根头发"
,那么替换后的句子就是:"一天掉多少根头发的发生,到底需要如何做到,不一天掉多少根头发的发生,又会如何产生。 "
。
function 来点论述(){
let 句子 = 随便取一句(论述);
句子 = 句子.replace(RegExp("主题", "g"),主题);
return 句子;
}
画个图:
1.6、增加段落
检查当前章节的文本的最后是否是一个空格,如果是,把章节的最后两个字符去掉:章节.slice(0,-2)
,为什么要这样做呢?因为配置的论述
和后面垫话
的末尾都是一个标点符号然后留一个空格,比如:"每个人都不得不面对这些问题。 在面对这种问题时, "
,如果在以此句作为章节的最后一句,则需要把,(空格)
替换为句号。
,章节的开头要空两个空格:" " + 章节 + "。 "
。
function 增加段落(章节){
if(章节[章节.length-1] === " "){
章节 = 章节.slice(0,-2)
}
return " " + 章节 + "。 "
}
画个图:
1.7、生成文章
获取input
的输入作为主体变量,
文章文字长度小于6000
字的情况下一直循环执行:
5%
的概率且文章字数大于200
字时,生成新的章节;
15%
的概率随机一句名人名言;
80%
的概率随机一句论述;
每个章节都塞入到文章中;
最后显示文章文本到浏览器中。
function 生成文章(){
主题 = $('input').value
let 文章 = []
for(let 空 in 主题){
let 章节 = "";
let 章节长度 = 0;
while( 章节长度 < 6000 ){
let 随机数 = 随便取一个数();
if(随机数 < 5 && 章节.length > 200){
章节 = 增加段落(章节);
文章.push(章节);
章节 = "";
}else if(随机数 < 20){
let 句子 = 来点名人名言();
章节长度 = 章节长度 + 句子.length;
章节 = 章节 + 句子;
}else{
let 句子 = 来点论述();
章节长度 = 章节长度 + 句子.length;
章节 = 章节 + 句子;
}
}
章节 = 增加段落(章节);
文章.push(章节);
}
let 排版 = "<div>" + 文章.join("</div><div>") + "</div>";
$("#论文").innerHTML = 排版;
}
画个图:
到这里,JavaScript
的源码我们就分析完了,其实整个逻辑很直接,不饶弯子,相信大家看一看就能看懂。下面我们再来看看Python
版的源码吧~
2、Python版源码分析
我们使用文本编辑器打开自动狗屁不通文章生成器.py
。
2.1、读取配置
可以看到,开头是使用readJSON
模块来读取data.json
配置,存到变量中,
import os, re
import random,readJSON
data = readJSON.读JSON文件("data.json")
名人名言 = data["famous"] # a 代表前面垫话,b代表后面垫话
前面垫话 = data["before"] # 在名人名言前面弄点废话
后面垫话 = data['after'] # 在名人名言后面弄点废话
废话 = data['bosh'] # 代表文章主要废话来源
我们可以打开data.json
看看,
格式是这样的:
{
"title":"主题",
"famous":[
"爱迪生a,天才是百分之一的勤奋加百分之九十九的汗水。b",
"查尔斯·史a,一个人几乎可以在任何他怀有无限热忱的事情上成功。b",
...
],
"bosh":[
"现在,解决x的问题,是非常非常重要的。所以,",
"我们不得不面对一个非常尴尬的事实,那就是,",
...
],
"before":[
"这不禁令我深思。",
"带着这句话,我们还要更加慎重的审视这个问题:",
...
],
"after":[
"曾经说过",
"在不经意间这样说过",
...
],
}
我们再看回readJSON.py
脚本,只有一个函数,就是先判断一下配置文件是不是.json
结尾的,然后open
配置文件,把文件内容read
进来,最后json.loads
把配置的文本(json
格式的字符串)转为python
的字典并return
,
def 读JSON文件(fileName=""):
import json
if fileName!='':
strList = fileName.split(".")
if strList[len(strList)-1].lower() == "json":
with open(fileName,mode='r',encoding="utf-8") as file:
return json.loads(file.read())
画个图:
2.2、对废话和名人名言进行洗牌
定义一个洗牌方法,然后对废话和名人名言进行洗牌,并返回迭代器,方便后续使用迭代器进行next
操作,
重复度 = 2
def 洗牌遍历(列表):
global 重复度
池 = list(列表) * 重复度
while True:
random.shuffle(池)
for 元素 in 池:
yield 元素
下一句废话 = 洗牌遍历(废话)
下一句名人名言 = 洗牌遍历(名人名言)
注意,上面首先是把列表进行了2
次重复,再进行洗牌。
例:
a = [1,2,3]
b = list(a) * 2
print(b)
# 输出[1, 2, 3, 1, 2, 3]
另外,上面用到了random.shuffle
函数,它是将序列的所有元素随机排序(即洗牌)。
2.3、来点名人名言
使用next
取出迭代器的下一个项,即下一句名人名言,然后对名言中的关键字"a"
和"b"
做替换:
"a"
替换为随机一句前面垫话
;
"b"
替换为随机一句后面垫话
;
def 来点名人名言():
global 下一句名人名言
xx = next(下一句名人名言)
xx = xx.replace("a", random.choice(前面垫话))
xx = xx.replace("b", random.choice(后面垫话))
return xx
注意,data.json
配置表中的名人名(famous
)言格式为:
"名人a,名言。b"
,
例:
"爱迪生a,天才是百分之一的勤奋加百分之九十九的汗水。b",
"查尔斯·史a,一个人几乎可以在任何他怀有无限热忱的事情上成功。b",
"培根说过,深窥自己的心,而后发觉一切的奇迹在你自己。b",
...
2.4、另起一段
另起一段就是给文章追加句号.
,换行"\r\n"
,新段落开头空四个空格,
def 另起一段():
xx = ". "
xx += "\r\n"
xx += " "
return xx
2.5、main入口:生成文章
提示输入文章主题,当文章字数小于1000
字时循环执行:5%
的概率另起一段,15%
的概率随机一句名人名言,80%
的概率随机一句废话。
最后替换关键字"x"
为主题,输出文章内容。
if __name__ == "__main__":
xx = input("请输入文章主题:")
for x in xx:
tmp = str()
while ( len(tmp) < 1000 ) :
分支 = random.randint(0,100)
if 分支 < 5:
tmp += 另起一段()
elif 分支 < 20 :
tmp += 来点名人名言()
else:
tmp += next(下一句废话)
tmp = tmp.replace("x",xx)
print(tmp)
画个图:
五、界面设计
好了,现在我们开始动手制作Unity
版本的狗屁不通文章生成器吧~
我们先使用axure
快速原型设计工具先简单设计一下界面,
六、UI素材获取
简单的UI
素材资源我是在阿里巴巴矢量图库上找,地址:https://www.iconfont.cn/
比如搜索按钮,
找一个形状合适的,可以进行调色,我一般是调成白色,
因为Unity
中可以设置Color
,这样我们只需要一个白色按钮就可以在Unity
中创建不同颜色的按钮了。
弄点基础的美术资源,
七、创建Unity工程
创建一个2D
模板的Unity
工程,工程名我定为UnityBullshitGenerator
,如下:
我想做成竖版的,Game
视图分辨率设置为720*1280
,
八、制作界面预设:MainPanel.prefab
把上面我们获取的UI
素材导入到Unity
工程中,放在Assets / Textures
目录中,如下:
注意图片类型设置为Sprite (2D and UI)
,
对部分特定的UI
素材使用Sprite Editor
进行九宫格切割,
接着,使用UGUI
制作界面预设:MainPanel.prefab
,保存到Assets /Prefabs
目录中,
预设节点结构如下:
预设显示如下:
九、配置表:data.json
创建data.json
,内容格式如下,可自行往配置中添加句子(注意有些句子是带可替换的字符的,比如a、b、x
)
{
"title":"主题",
"famous":[
"爱迪生a,天才是百分之一的勤奋加百分之九十九的汗水。b",
"查尔斯·史a,一个人几乎可以在任何他怀有无限热忱的事情上成功。b",
...
],
"bosh":[
"现在,解决x的问题,是非常非常重要的。所以,",
"我们不得不面对一个非常尴尬的事实,那就是,",
...
],
"before":[
"这不禁令我深思。",
"带着这句话,我们还要更加慎重的审视这个问题:",
...
],
"after":[
"曾经说过",
"在不经意间这样说过",
...
],
}
将data.json
保存到Unity
工程的Assets/Resources
目录中:
十、程序代码
程序部分,分为Logic
和View
,Logic
实现逻辑,View
实现界面交互。
BullshitGenerator.cs
和MainPanel.cs
都很好写,难点是Utils.cs
:文字如何转Texture2D
(文字长图)。不要怕,稍作研究就可以做出来滴,我们先把简单的做了~
1、BullshitGenerator.cs脚本
在Assets / Scripts / Logic
目录中创建BullshitGenerator.cs
脚本,
BullshitGenerator.cs
脚本要具体做什么呢?首先画一下思维导图:
1.1、使用LitJson解析配置
我们要解析json
格式的配置表,需要使用Json
库,我推荐使用LitJson
开源库,可以从GitHub
上下载,LitJson
开源项目地址:https://hub.fastgit.org/LitJSON/litjson
我们下载下来后,把src
目录中的LitJson
文件夹整个拷贝到我们Unity
工程中,如下:
接着我们就可以在代码中使用LitJson
了,使用时引用命名空间
using LitJson;
我们先定义一些容器,用于存放配置表的内容,
// BullshitGenerator.cs
/// <summary>
/// 名人名言
/// </summary>
private List<string> m_famous = new List<string>();
/// <summary>
/// 废话
/// </summary>
private List<string> m_bosh = new List<string>();
/// <summary>
/// 前面垫话
/// </summary>
private List<string> m_after = new List<string>();
/// <summary>
/// 后面垫话
/// </summary>
private List<string> m_before = new List<string>();
封装一个LoadCfg
方法,实现配置表读取的逻辑,
// BullshitGenerator.cs
/// <summary>
/// 读取配置表
/// </summary>
public void LoadCfg()
{
// 读取配置文件(json格式的字符串)
var jsonText = Resources.Load<TextAsset>("data").text;
// 转为JsonData
var jd = JsonMapper.ToObject(jsonText);
// 名人名言
m_famous = JsonMapper.ToObject<List<string>>(jd["famous"].ToJson());
// 废话
m_bosh = JsonMapper.ToObject<List<string>>(jd["bosh"].ToJson());
// 前垫话
m_before = JsonMapper.ToObject<List<string>>(jd["before"].ToJson());
// 后垫话
m_after = JsonMapper.ToObject<List<string>>(jd["after"].ToJson());
}
1.2、List洗牌
为了让名人名言和废话的获取具有随机性,我们写一个洗牌函数:
// BullshitGenerator.cs
/// <summary>
/// 对List进行洗牌
/// </summary>
private void Shuffle(ref List<string> list)
{
var r = new System.Random();
for (int i = list.Count - 1; i >= 0; i--)
{
int cardIndex = r.Next(i);
var temp = list[cardIndex];
list[cardIndex] = list[i];
list[i] = temp;
}
}
对名人名言和废话执行一次洗牌:
// BullshitGenerator.cs
// 对名人名言进行洗牌
Shuffle(ref m_famous);
// 对废话进行洗牌
Shuffle(ref m_bosh);
1.3、另起一段
封装一个StartNewLine
函数,实现另起一段的功能(末尾标点符号替换为句号。
并追加一个换行符\n
),
// BullshitGenerator.cs
/// <summary>
/// 开始新的一行,会将传入的文本的最后一个标点符号替换为句号然后换行(\n)
/// </summary>
/// <param name="txt">传入的文本</param>
/// <returns>处理后的文本</returns>
private string StartNewLine(string txt)
{
if (txt.Length > 1)
return txt.Substring(0, txt.Length - 1) + "。\n";
return txt;
}
1.4、获取一句名人名言
封装一个GetFamous
方法,实现获取下一句名人名言的功能,
/// <summary>
/// 获取一句名人名言
/// </summary>
private string GetFamous()
{
// 取下一句名人名言
var txt = Next(m_famous, ref m_curFamousIndex);
// 替换前垫话
txt = txt.Replace("a", RandomChoice(m_before));
// 替换后垫话
txt = txt.Replace("b", RandomChoice(m_after));
return txt;
}
/// <summary>
/// 当前名人名言索引
/// </summary>
private int m_curFamousIndex = 0;
其中Next
方法如下,实现从List
中取下一个索引的值,
// BullshitGenerator.cs
/// <summary>
/// 取List的下一个索引的值
/// </summary>
/// <param name="list">List对象</param>
/// <param name="index">当前索引</param>
/// <returns></returns>
private string Next(List<string> list, ref int index)
{
if (index < list.Count - 1)
{
++index;
}
else
{
index = 0;
}
var result = list[index];
return result;
}
RandomChoice
方法如下,实现从List
随机取一个值的功能,
// BullshitGenerator.cs
/// <summary>
/// 对List进行随机获取一个值
/// </summary>
/// <param name="list">List对象</param>
/// <returns></returns>
private string RandomChoice(List<string> list)
{
return list[Random.Range(0, list.Count)];
}
1.5、获取一句废话
// BullshitGenerator.cs
/// <summary>
/// 当前废话索引
/// </summary>
private int m_curBoshIndex = 0;
// ...
// 获取一句废话
var bosh = Next(m_bosh, ref m_curBoshIndex);
1.6、段首空两格
我们需要对文字段落进行格式化,把英文的空格替换为中文的空格符\u3000
,否则文字排版可能会出问题,如下:
// BullshitGenerator.cs
/// <summary>
/// 格式化空格(替换为中文的空格符:\u3000,并处理段首自动空两格)
/// </summary>
/// <param name="text">要处理的文本</param>
/// <returns>处理后的文本</returns>
private string FormatSpace(string text)
{
// 开头两个空白符
var temp_content = "\u3000\u3000" + text;
// 新的一行缩进两个空白符
temp_content = temp_content.Replace("\n", "\n\u3000\u3000");
// 英文空格转中文空白符
temp_content = temp_content.Replace(" ", "\u3000");
return temp_content;
}
1.7、生成文章
封装一个DoGen
函数,实现生成文章的功能,
/// <summary>
/// 根据标题生成狗屁不通文章
/// </summary>
/// <param name="title">标题</param>
/// <returns>生成的文章</returns>
public string DoGen(string title)
{
string tmp = "";
while (tmp.Length < 1000)
{
var rd = Random.Range(0, 100);
if (rd < 5)
{
// 下一行
tmp = StartNewLine(tmp);
}
else if (rd < 20)
{
// 取一句名人名言
tmp += GetFamous();
}
else
{
// 下一句废话
tmp += Next(m_bosh, ref m_curBoshIndex);
}
}
// 文末空一行
tmp = StartNewLine(tmp);
// 给标题关键字标注颜色
tmp = tmp.Replace("x", "<color=#960000ff>" + title + "</color>");
// 格式化空格
tmp = FormatSpace(tmp);
return tmp;
}
2、MainPanel.cs脚本
在Assets / Scripts / View
目录中创建MainPanel.cs
脚本,
2.1、UI成员变量
先定义一些必要的UI
成员变量,
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class MainPanel : MonoBehaviour
{
/// <summary>
/// 主题输入框
/// </summary>
public InputField titleInput;
/// <summary>
/// 生成文章按钮
/// </summary>
public Button generateBtn;
/// <summary>
/// 保存图片按钮
/// </summary>
public Button saveBtn;
/// <summary>
/// 文章Text
/// </summary>
public Text articleText;
// ...
}
2.2、初始化
在Awake
中做一下初始化,调用配置加载接口,
// MainPanel.cs
/// <summary>
/// 文章生成器
/// </summary>
private BullshitGenerator m_generator;
private void Awake()
{
m_generator = new BullshitGenerator();
// 加载配置
m_generator.LoadCfg();
}
2.3、生成文章按钮的点击逻辑
在Start
函数中,实现生成文章按钮的点击逻辑,调用BullshitGenerator
的DoGen
来生成文章,
// MainPanel.cs
void Start()
{
string generateText = "";
// 生成文章
generateBtn.onClick.AddListener(() =>
{
generateText = m_generator.DoGen(titleInput.text);
articleText.text = generateText;
});
}
3、测试文章生成
把MainPanel.cs
脚本挂到MainPanel.prefab
预设上,并赋值成员变量。
运行Unity
,测试效果如下,可以正常生成狗屁不通的文章~
十一、UGUI的Text如何转成Texture2D长图
现在,我们要将文字保存为长图,也就是把UGUI
的Text
转成Texture2D
,再把Texture2D
存为本地的jpg
文件。
我的思路利用自定义字体生成字体纹理贴图,通过文字的uv
坐标信息获取文字的像素信息,然后绘制到我们动态生成的Texture2D
对象上。
1、制作字体
首先,我们找一个ttf
字体,放到Unity
工程的Assets / Fonts
文件夹中,
将字体的Character
设置为Custom Set
,
在字体统计目录中创建一个character.txt
,
把英文字母、数字、标点符号、常用汉字放到这个character.txt
文件中,如下:
然后把整个文本内容复制,回到Unity
中,粘贴到Font
的Custom Chars
中,点击Apply
,
注:上面存为
character.txt
文件不是必要的,但建议你存成文件,方便你查找或修改字符。
此时字体会生成一个材质和纹理贴图,
字太多了,这样看看不大清,
我们先弄少一点字符,
看它生成的纹理贴图,我们可以看到,我们的字有的是上下颠倒的,有的是顺时针旋转了90
度,(经过我多次测试,发现它只有这两种情况),这个我们后面写文字长图生成的时候需要小心了。
2、获取字体纹理贴图
var fontTexture = (Texture2D)font.material.mainTexture;
不过字体纹理本身不可读,我们无法对其调用GetPixels
函数,所以我们需要拷贝一份可读的,
// 字体贴图不可读,需要创建一个新的可读的
var fontTexture = (Texture2D)font.material.mainTexture;
var readableFontTexture = new Texture2D(fontTexture.width,
fontTexture.height,
fontTexture.format,
fontTexture.mipmapCount,
true);
Graphics.CopyTexture(fontTexture, readableFontTexture);
3、根据字符获取对应纹理uv、宽高信息等
char charitem = "哈";
font.GetCharacterInfo(charitem, out CharacterInfo info);
从CharacterInfo
中我们就可以获取到字符的详细信息,包括uv
、宽高等信息,
4、判断字符纹理是否是上下颠倒和顺时针90度
我们上面可以看到,字符纹理有的是上下颠倒的,我们可以使用uvTopLeft
和uvBottomRight
来判断
if (info.uvTopLeft.y < info.uvBottomRight.y)
{
// 字符是上下颠倒的
}
else
{
// 顺时针旋转90度的
}
5、获取字符的宽高
字符的实际宽高的获取要小心了,如果是顺时针旋转90
度的字符,则宽和高是对调的,
int charWidth, charHeight;// 字符宽高
if (info.uvTopLeft.y < info.uvBottomRight.y)
{
// 字符是上下颠倒的
charWidth = info.glyphWidth;
charHeight = info.glyphHeight;
}
else
{
// 顺时针旋转90度的
charWidth = info.glyphHeight;
charHeight = info.glyphWidth;
}
6、获取字符的像素信息
我们上面已经得到了可读的字体纹理贴图(Texture2D
),又拿到了字符的uv
坐标、字符宽高信息,那么,我们就可以通过Texture2D
的GetPixels
接口获取到字符的像素信息了,
public Color[] GetPixels(int x, int y, int blockWidth, int blockHeight);
上下颠倒的字符的像素信息的获取:
Color[] charColor = readableFontTexture.GetPixels(
(int)(readableFontTexture.width * info.uvTopLeft.x),
(int)(readableFontTexture.height * info.uvTopLeft.y),
charWidth, charHeight);
顺时针旋转90
度的字符的像素信息的获取:
charColor = readableFontTexture.GetPixels(
(int)(readableFontTexture.width * info.uvBottomRight.x),
(int)(readableFontTexture.height * info.uvBottomRight.y),
charWidth, charHeight);
7、如何给Texture2D写入像素
我们要创建一个长图(背景白色),然后给这个长图挨个字挨个字写入像素,所以,我们先创建一个Texture2D
,并填充背景色,
// 背景色
Color backgroundColor = Color.white;
// 创建Texture2D
var textTexture = new Texture2D(textureWidth, textureHeight, TextureFormat.ARGB32, true);
// 填充背景色
Color[] emptyColor = new Color[textureWidth * textureHeight];
for (int i = 0; i < emptyColor.Length; i++)
{
emptyColor[i] = backgroundColor;
}
textTexture.SetPixels(emptyColor);
接着我们就可以使用SetPixels
接口给这个长图Texture2D
挨个字挨个字写入字符像素了,
// 逐个字符绘制
foreach (var charitem in text.ToCharArray())
{
// ...
textTexture.SetPixel(x, y, color);
}
这里我们需要自己维护一个光标坐标,要控制好光标坐标不能超过Texture2D
外面,还有每个字符的像素尺寸是有大有小的,我们需要计算中心对齐,不然字符就会高高低低(标点符号除外,否则比如逗号就会和文字是中心对齐的,实际上逗号要在文字的脚下)
8、最终Utils的TextToTexture2D接口
在Assets / Scripts / Logic
目录中新建一个Utils.cs
脚本,封装一个TextToTexture2D
接口,如下:
// Utils.cs
/// <summary>
/// UGUI的Text转Texture2D
/// </summary>
/// <param name="font">字体</param>
/// <param name="text">Text的文本</param>
/// <param name="textureWidth">图片的宽</param>
/// <param name="textureHeight">图片的高(最终会以字体的排版而裁剪掉多余的空白部分)</param>
/// <param name="drawOffsetX">要渲染的文字的x坐标偏移</param>
/// <param name="drawOffsetY">要渲染的文字的y坐标偏移</param>
/// <param name="textGap">每个字之间的间隔</param>
/// <param name="spaceGap">空白符的间隔</param>
/// <param name="fontSize">字体大小</param>
/// <param name="textColor">文字颜色</param>
/// <param name="backgroundColor">背景图颜色</param>
/// <returns></returns>
public static Texture2D TextToTexture2D(
Font font,
string text,
int textureWidth, int textureHeight,
int drawOffsetX, int drawOffsetY,
int textGap, int spaceGap, int fontSize,
Color textColor,
Color backgroundColor)
{
// 创建返回的Texture
var textTexture = new Texture2D(textureWidth, textureHeight, TextureFormat.ARGB32, true);
Color[] emptyColor = new Color[textureWidth * textureHeight];
for (int i = 0; i < emptyColor.Length; i++)
{
emptyColor[i] = backgroundColor;
}
textTexture.SetPixels(emptyColor);
// 字体贴图不可读,需要创建一个新的可读的
var fontTexture = (Texture2D)font.material.mainTexture;
var readableFontTexture = new Texture2D(fontTexture.width, fontTexture.height, fontTexture.format, fontTexture.mipmapCount, true);
Graphics.CopyTexture(fontTexture, readableFontTexture);
// 调整偏移量
var originalDrawOffsetX = drawOffsetX; // 记录一下,换行用
drawOffsetY = textureHeight - drawOffsetY - fontSize; // 从上方开始画
// 逐个字符绘制
foreach (var charitem in text.ToCharArray())
{
if ('\u3000' == charitem || ' ' == charitem)
{
drawOffsetX += spaceGap;
continue;
}
if ('\n' == charitem)
{
// 换行
drawOffsetX = originalDrawOffsetX;
drawOffsetY -= fontSize;
continue;
}
// 判断是否是中文
bool isChinese = false;
if (charitem >= 0x4e00 && charitem <= 0x9fbb)
{
isChinese = true;
}
if (drawOffsetX >= textTexture.width - fontSize)
{
// 换行
drawOffsetX = originalDrawOffsetX;
drawOffsetY -= fontSize;
}
int charWidth, charHeight;// 字符宽高
Color[] charColor;// 字符颜色,数组内颜色的顺序为从左至右,从下至上
font.GetCharacterInfo(charitem, out CharacterInfo info);
if (info.uvTopLeft.y < info.uvBottomRight.y)// 处理被垂直翻转的字符
{
charWidth = info.glyphWidth;
charHeight = info.glyphHeight;
charColor = readableFontTexture.GetPixels(
(int)(readableFontTexture.width * info.uvTopLeft.x),
(int)(readableFontTexture.height * info.uvTopLeft.y),
charWidth, charHeight);
for (int j = 0; j < charHeight; j++)
{
for (int i = 0; i < charWidth; i++)
{
if (charColor[j * charWidth + i].a != 0)
{
// 从上往下画,把字符颠倒过来
textTexture.SetPixel(
drawOffsetX + i,
drawOffsetY + charHeight - j + (isChinese ? ((int)((fontSize - charHeight) / 2f)) : 0),
textColor);
}
}
}
drawOffsetX += charWidth + textGap;
}
else // 处理被顺时针旋转90度的字符
{
charWidth = info.glyphHeight;
charHeight = info.glyphWidth;
charColor = readableFontTexture.GetPixels(
(int)(readableFontTexture.width * info.uvBottomRight.x),
(int)(readableFontTexture.height * info.uvBottomRight.y),
charWidth, charHeight);
for (int j = 0; j < charHeight; j++)
{
for (int i = 0; i < charWidth; i++)
{
if (charColor[j * charWidth + i].a != 0)
{
// 旋转
textTexture.SetPixel(
drawOffsetX + charHeight - j,
drawOffsetY + i + (isChinese ? ((int)((fontSize - charWidth) / 2f)) : 0),
textColor);
}
}
}
drawOffsetX += charHeight + textGap;
}
}
var realTextureHeight = textureHeight - drawOffsetY;
textTexture.Apply();
var finalTexture = new Texture2D(textureWidth, realTextureHeight, TextureFormat.ARGB32, true);
Graphics.CopyTexture(textTexture, 0, 0, 0, drawOffsetY, textureWidth, realTextureHeight, finalTexture, 0, 0, 0, 0);
Object.Destroy(textTexture);
Object.Destroy(readableFontTexture);
return finalTexture;
}
十二、Texture2D长图存为本地jpg文件
我们在Utils
脚本中封装一个SaveTextureToLocal
方法,这个比较简单,就不细讲了~
// Utils.cs
/// <summary>
/// 将Texture2D保存到本地文件
/// </summary>
/// <param name="texture">Texture2D对象</param>
/// <param name="fileName">要保存的文件名</param>
public static string SaveTextureToLocal(Texture2D texture, string fileName)
{
var bytes = texture.EncodeToJPG();
var savePath = Application.persistentDataPath + "/" + fileName;
#if UNITY_EDITOR
savePath = Application.dataPath + "/" + fileName;
#endif
File.WriteAllBytes(savePath, bytes);
Debug.Log("SaveTextureToLocal: " + savePath);
return savePath;
}
十三、测试文字长图的生成
在MainPanel.cs
脚本中的保存图片按钮的响应中添加Utils
的调用,
// MainPanel.cs
public GameObject waitObj;
public GameObject tipsObj;
public Text tipsText;
private Coroutine saveTextureCoroutine;
private void Start()
{
// ...
// 保存为图片
saveBtn.onClick.AddListener(() =>
{
if (null != saveTextureCoroutine)
StopCoroutine(saveTextureCoroutine);
saveTextureCoroutine = StartCoroutine(SaveTexture(generateText));
});
}
private IEnumerator SaveTexture(string generateText)
{
waitObj.SetActive(true);
yield return null;
// 对文字做些处理
generateText = generateText.Replace("<color=#960000ff>", "").Replace("</color>", "");
generateText = string.Format("题目:{0}\n{1}\n博主:林新发\n博客:https://blog.csdn.net/linxinfa\n", titleInput.text, generateText);
// 文字转Texture2D
var texture2D = Utils.TextToTexture2D(articleText.font, generateText,
(int)articleText.rectTransform.rect.width,
(int)articleText.rectTransform.rect.height, 10, 10, 1,
articleText.fontSize, articleText.fontSize, Color.black, Color.white);
// 保存为jpg
var savePath = Utils.SaveTextureToLocal(texture2D, "article.jpg");
// 提示保存路径
tipsObj.SetActive(true);
tipsText.text = "保存成功,路径:\n" + savePath;
waitObj.SetActive(false);
// 2秒后自动隐藏提示
yield return new WaitForSeconds(2);
tipsObj.SetActive(false);
}
把MainPanel.prefab
中显示文章文本的Text
的字体设置为我们自定义的字体,
运行Unity
,生成文章,点击保存图片,
生成的长图如下(如果觉得不错,记得给我点个赞呀,我可是调试测试了好多次T_T
):
十四、工程源码
本文工程源码我一上传到CODE CHINA
,感兴趣的同学可自行下载下来学习。
地址:https://codechina.csdn.net/linxinfa/unitybullshitgenerator
注:我使用的Unity
版本为:2021.1.7f1c1
十五、完毕
好了,就写到这里吧~
我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信~