前言
在某些游戏中,有一些让人感到意味不明的未知符号,例如在游戏《巴别塔圣歌》中,就有这样一些能让人在初次就看不懂的未知符号。

或者在其他时候,这些未知符号如果跟粒子系统结合在一起的话,也可以当作是一种特效,可以增加游戏的神秘感。
而现在,如果你想学的话,那么现在就可以开始学了。
方法1
首先讲最简单的方法,最简单的就先随机地显示Wingdings字体的字符,然后在随机显示字符后等待一小会即可 ,这个对于很多人来说都容易去理解,但是,这个方法主要的难点是如何将TextMeshPro
中的字体设为Wingdings字体 ,因此,接下来就要详细的讲一下如何更改TextMeshPro
组件的字体。
-
打开文件资源管理器;
-
转到C:\Windows\Fonts ,选取里面的Wingdings 常规;

- 将Wingdings 常规 拖到unity的项目栏里,并右键 选中拖过来的字体,创建
TextMeshPro
的Wingdings字体资产。

- 将
TextMeshPro
组件的字体属性*(Font Asset)* 设为刚才得到的Wingdings字体资产,TextMeshPro
组件的字体也更改好了。

其他只要你学过unity,实现起来就简单,这里就直接上方法了*(想用这个方法得需要TextMeshPro
组件)*。
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Collections;
using Unity.VisualScripting;
using System.Runtime.CompilerServices;
using TMPro;
using System.Net;
using System.Net.Sockets;
public class randomSymbolSummon1 : MonoBehaviour
{
public float waitTime = 0.1f;
private bool isEnd = true;
private char[] chars;
IEnumerator write()
{
isEnd = false;
GetComponent<TextMeshPro>().text = chars[Random.Range(0, chars.Length)].ToString();
yield return new WaitForSeconds(waitTime);
isEnd = true;
}
// Start is called before the first frame update
void Start()
{
if (null == GetComponent<TextMeshPro>())
{
Debug.LogError("错误:没有TextMeshPro组件");
}
chars = new char[127 - 33];
char ch = (char)33;
int offset = 0;
for (; ch < 127; ch++)
{
if ('~' == ch || '{' == ch)
{
offset--;
continue;
}
chars[ch - 33] = ch;
}
write();
}
// Update is called once per frame
void Update()
{
if (isEnd)
{
StartCoroutine(write());
}
}
}
下面就是最简单的方法的效果。

方法2
然后来讲难一点的方法,难一点的方法呢,它的核心就是一个可以当作是用于写符号的笔的指针在点上随机移动。
首先,我们要定义一些公有的成员变量。就如点离点的距离,由点组成的"画板"的长与宽,是否只显示一次符号,线的渲染,线的宽度及等待时间。然后,新建一个summon
方法,这将是我们生成方法2中的符号的核心方法。
csharp
public float space = 0.809f;
public uint a = 2;
public uint b = 3;
public Material lineMaterial;
public float lineWidth = 0.1f;
public float waitTime = 0.1f;
private bool isEnd = true;
private void summon()
{
}
然后,我们在Start
方法里调用一下summon
方法,这将使对象一开始就能生成未知符号,并在Update
函数里面设置执行summon
方法的条件,只有能显示多次符号,并且等会实现的write
协程已经执行完毕,那么就才能执行write
协程。
write
协程,我们要让它能等待,就需要yield return
和新定义的isEnd
变量,因此,刚才就需要什么方法就很明了了。顺带一提醒,就是Start
方法及Update
等方法都不能是协程,这点unity萌新一定要注意!
csharp
void Start()
{
summon();
}
void Update()
{
if (!summonOneShot && isEnd)
{
StartCoroutine(write());
}
}
接着,就要实现主要的summon
方法了,首先根据"画板"的长与宽和点离点的距离,我们定义一个数组,数组大小为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( a + 1 ) ( b + 1 ) (a+1)(b+1) </math>(a+1)(b+1)。为了省事,可以在Start
方法内让a
和b
各自增1。并通过指针来一个个的设置点的位置。
csharp
private void summon()
{
Vector3[] dots = new Vector3[a * b];
for (int doti = 0; doti < dots.Length; ++doti)
{
dots[doti] = new Vector3(doti % a * space, doti / a * -space, 0);
}
}
void Start()
{
a++;
b++;
summon();
}
之后定义一个Vector3
的paths
可变数组,一个有8个bool
的canMove
数组,和一个有8个int
的modeMove
数组,初始化modeMove
数组时,将modeMove
数组第3项和第4项分别设为-1和1。
随后,我们知道,在二维数组中,指针移动二维数组的长度位,就是将指针以垂直方向移动。因此,就可以用变量a
来设置modeMove
数组。
csharp
private void summon()
{
Vector3[] dots = new Vector3[a * b];
for (int doti = 0; doti < dots.Length; ++doti)
{
dots[doti] = new Vector3(doti % a * space, doti / a * -space, 0);
}
List<Vector3> paths = new List<Vector3>();
bool[] canMove = new bool[8];
int[] modeMove = { 0, 0, 0, -1, 1, 0, 0, 0 };
for (int modei = 0, step = (int)a + 1; modei < 3; ++modei) {
modeMove[modei] = -step;
modeMove[modeMove.Length - 1 - modei] = step--;
}
}
我们需要设定一个绘制符号的结束条件,结束条件呢,就是这个"笔"移到了"画板"每一行及每一列的点,因此,我们就定义一个bool
类型的大小为 <math xmlns="http://www.w3.org/1998/Math/MathML"> a + b a+b </math>a+b的dotMove
数组,如果dotMove
数组全是真,那么等于"笔"移到了第 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 ∼ a 1\sim a </math>1∼a列的所有点和第 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 ∼ b 1\sim b </math>1∼b行的所有点了。就需要一个check
方法来检测dotMove
是否全是真。
且结束条件为真时,由于"笔"可能还有一次画的操作,但是"笔"不能回头,所以就也要定义lastDraw
的变量和mode
变量了。
csharp
private bool check(bool[] dotMove)
{
foreach (bool b in dotMove)
{
if (!b)
{
return false;
}
}
return true;
}
private void summon()
{
Vector3[] dots = new Vector3[a * b];
for (int doti = 0; doti < dots.Length; ++doti)
{
dots[doti] = new Vector3(doti % a * space, doti / a * -space, 0);
}
List<Vector3> paths = new List<Vector3>();
bool[] canMove = new bool[8];
int[] modeMove = { 0, 0, 0, -1, 1, 0, 0, 0 };
for (int modei = 0, step = (int)a + 1; modei < 3; ++modei) {
modeMove[modei] = -step;
modeMove[modeMove.Length - 1 - modei] = step--;
}
bool[] dotMove = new bool[a + b];
int mode = 0;
bool LastDraw = 1 == Random.Range(0, 2);
while (check(dotMove) || LastDraw)
{
if (check(dotMove))
{
canMove[7 - mode] = false;
LastDraw = false;
}
}
}
而在summon
方法中,我们要对"笔"移动到的点检测一下,假如dots
是一个长为 <math xmlns="http://www.w3.org/1998/Math/MathML"> a + 1 a+1 </math>a+1,宽为 <math xmlns="http://www.w3.org/1998/Math/MathML"> b + 1 b+1 </math>b+1的二维数组,就要让pos
指针分别去通过运算符%
和/
与 <math xmlns="http://www.w3.org/1998/Math/MathML"> a + 1 a+1 </math>a+1进行运算,从而得到pos
在dots
二维数组中的坐标,就可以设置dots
数组项的值。
chsarp
private bool check(bool[] dotMove)
{
foreach (bool b in dotMove)
{
if (!b)
{
return false;
}
}
return true;
}
private void summon()
{
Vector3[] dots = new Vector3[a * b];
for (int doti = 0; doti < dots.Length; ++doti)
{
dots[doti] = new Vector3(doti % a * space, doti / a * -space, 0);
}
List<Vector3> paths = new List<Vector3>();
bool[] canMove = new bool[8];
int[] modeMove = { 0, 0, 0, -1, 1, 0, 0, 0 };
for (int modei = 0, step = (int)a + 1; modei < 3; ++modei) {
modeMove[modei] = -step;
modeMove[modeMove.Length - 1 - modei] = step--;
}
bool[] dotMove = new bool[a + b];
bool LastDraw = 1 == Random.Range(0, 2);
while (check(dotMove) || LastDraw)
{
if (check(dotMove))
{
canMove[7 - mode] = false;
LastDraw = false;
}
dotMove[pos % a] = true;
dotMove[a + pos / a] = true;
}
}
之后,我们定义一个pos
变量,将他的值设为0到 <math xmlns="http://www.w3.org/1998/Math/MathML"> a b − 1 ab-1 </math>ab−1内的随机值,这代表了"笔"的位置,并将这个位置传到paths
数组里,作为方法2中符号的起点。再接着,我们就设置"笔"要移动的方向。
"笔"方向有8种,"↖↑↗←→↙↓↘"这些全都是*(分别代表了0~7的数字,这以后会用到)* 。因此,刚才定义的canMove
和modeMove
数组大小都为8个,并且,假设有一个方向 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x,因为 <math xmlns="http://www.w3.org/1998/Math/MathML"> x + x 反 = 7 x+x_反=7 </math>x+x反=7,那么它的反方向就是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 7 − x 7-x </math>7−x,因此,刚才就将canMove[7 - mode]
设为假,就不让它回头了。
转到正题,要设置"笔"要移动的方向,就要检测这8个方向中可以移动的方向,就要用到canMove
数组中的每一项作标记。
如果某个方向不能移动,那么就说明到了dots
数组的边界旁,假如让一个指针分别去通过运算符%
和/
与二维数组的长进行运算,那么可以获得二维数组的坐标,我们就可以设置canMove
数组。
csharp
private bool check(bool[] dotMove)
{
foreach (bool b in dotMove)
{
if (!b)
{
return false;
}
}
return true;
}
private void summon()
{
Vector3[] dots = new Vector3[a * b];
for (int doti = 0; doti < dots.Length; ++doti)
{
dots[doti] = new Vector3(doti % a * space, doti / a * -space, 0);
}
List<Vector3> paths = new List<Vector3>();
bool[] canMove = new bool[8];
int[] modeMove = { 0, 0, 0, -1, 1, 0, 0, 0 };
for (int modei = 0, step = (int)a + 1; modei < 3; ++modei) {
modeMove[modei] = -step;
modeMove[modeMove.Length - 1 - modei] = step--;
}
bool[] dotMove = new bool[a + b];
bool LastDraw = 1 == Random.Range(0, 2);
while (check(dotMove) || LastDraw)
{
canMove[1] = (0 != pos / a);
canMove[3] = (0 != pos % a);
canMove[4] = ((a - 1) != pos % a);
canMove[6] = ((b - 1) != pos / a);
canMove[0] = (canMove[1] && canMove[3]);
canMove[2] = (canMove[1] && canMove[4]);
canMove[5] = (canMove[6] && canMove[3]);
canMove[7] = (canMove[6] && canMove[4]);
if (check(dotMove))
{
canMove[7 - mode] = false;
LastDraw = false;
}
dotMove[pos % a] = true;
dotMove[a + pos / a] = true;
}
}
那么,我们就因此定义一个mode
变量来从这些可以移动的方向中选取其中的一个让pos
移动了,在pos
移动之后,我们就将pos
移动到的点添加进paths
数组里,一次pos
移动的操作就完成了。以此往复下去,一个完全随机的paths
数组生成了。
csharp
private bool check(bool[] dotMove)
{
foreach (bool b in dotMove)
{
if (!b)
{
return false;
}
}
return true;
}
private void summon()
{
Vector3[] dots = new Vector3[a * b];
for (int doti = 0; doti < dots.Length; ++doti)
{
dots[doti] = new Vector3(doti % a * space, doti / a * -space, 0);
}
List<Vector3> paths = new List<Vector3>();
bool[] canMove = new bool[8];
int[] modeMove = { 0, 0, 0, -1, 1, 0, 0, 0 };
for (int modei = 0, step = (int)a + 1; modei < 3; ++modei) {
modeMove[modei] = -step;
modeMove[modeMove.Length - 1 - modei] = step--;
}
bool[] dotMove = new bool[a + b];
int mode = 0;
bool LastDraw = 1 == Random.Range(0, 2);
while (check(dotMove) || LastDraw)
{
canMove[1] = (0 != pos / a);
canMove[3] = (0 != pos % a);
canMove[4] = ((a - 1) != pos % a);
canMove[6] = ((b - 1) != pos / a);
canMove[0] = (canMove[1] && canMove[3]);
canMove[2] = (canMove[1] && canMove[4]);
canMove[5] = (canMove[6] && canMove[3]);
canMove[7] = (canMove[6] && canMove[4]);
if (check(dotMove))
{
canMove[7 - mode] = false;
LastDraw = false;
}
dotMove[pos % a] = true;
dotMove[a + pos / a] = true;
mode = Random.Range(0, 8);
while (!canMove[mode])
{
mode = Random.Range(0, 8);
}
pos += modeMove[mode];
paths.Add(dots[pos]);
}
}
此时,就可以用这个paths
数组来设置Unity组件LineRenderer
的线条了,不过要设置线条,得先设置线条端点的数量,后设置线条,由于pos
移动的路径就是线条,用paths
数组设置线条就很合适。
csharp
private bool check(bool[] dotMove)
{
foreach (bool b in dotMove)
{
if (!b)
{
return false;
}
}
return true;
}
private void summon()
{
Vector3[] dots = new Vector3[a * b];
for (int doti = 0; doti < dots.Length; ++doti)
{
dots[doti] = new Vector3(doti % a * space, doti / a * -space, 0);
}
List<Vector3> paths = new List<Vector3>();
bool[] canMove = new bool[8];
int[] modeMove = { 0, 0, 0, -1, 1, 0, 0, 0 };
for (int modei = 0, step = (int)a + 1; modei < 3; ++modei) {
modeMove[modei] = -step;
modeMove[modeMove.Length - 1 - modei] = step--;
}
bool[] dotMove = new bool[a + b];
int mode = 0;
bool LastDraw = 1 == Random.Range(0, 2);
while (check(dotMove) || LastDraw)
{
canMove[1] = (0 != pos / a);
canMove[3] = (0 != pos % a);
canMove[4] = ((a - 1) != pos % a);
canMove[6] = ((b - 1) != pos / a);
canMove[0] = (canMove[1] && canMove[3]);
canMove[2] = (canMove[1] && canMove[4]);
canMove[5] = (canMove[6] && canMove[3]);
canMove[7] = (canMove[6] && canMove[4]);
if (check(dotMove))
{
canMove[7 - mode] = false;
LastDraw = false;
}
dotMove[pos % a] = true;
dotMove[a + pos / a] = true;
mode = Random.Range(0, 8);
while (!canMove[mode])
{
mode = Random.Range(0, 8);
}
pos += modeMove[mode];
paths.Add(dots[pos]);
}
GetComponent<LineRenderer>().positionCount = paths.Count;
GetComponent<LineRenderer>().SetPositions(paths.ToArray());
}
做到这里,还有几个问题没有解决。
- 变量
a
或b
不能为0,space
不能为0。
解决方法:只需要在Start
方法一开始判断a
,b
或space
为0即可,如果条件达成,就将它们设为默认值。
csharp
void Start()
{
a = (0 == a ? 2 : a + 1);
b = (0 == a ? 3 : b + 1);
space = (0 == space ? 0.809f : space);
summon();
}
- 有时对象并没有
LineRenderer
组件,却仍要获取对象的LineRenderer
组件,并且必须将LineRenderer
使用世界空间的开关关掉。
解决方法:如果没有LineRenderer
组件,就为它进行初始化,并强制将组件的useWorldSpace
给设为假,让符号一直在物体上。
csharp
void Start()
{
a = (0 == a ? 2 : a + 1);
b = (0 == a ? 3 : b + 1);
space = (0 == space ? 0.809f : space);
if (null == GetComponent<LineRenderer>())
{
transform.AddComponent<LineRenderer>();
GetComponent<LineRenderer>().material = lineMaterial;
GetComponent<LineRenderer>().startWidth = GetComponent<LineRenderer>().endWidth = lineWidth;
}
GetComponent<LineRenderer>().useWorldSpace = false;
summon();
}
这些解决方法弄完之后,也就可以开始看方法2中生成的符号是什么样了。下面就是方法2的效果。

脚本
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Collections;
using Unity.VisualScripting;
using System.Runtime.CompilerServices;
public class randomSymbolSummon : MonoBehaviour
{
public float space = 0.809f;
public uint a = 2;
public uint b = 3;
public Material lineMaterial;
public float lineWidth = 0.1f;
public float waitTime = 0.1f;
private bool isEnd = true;
public bool summonOneShot = false;
private bool check(bool[] dotMove)
{
foreach (bool b in dotMove)
{
if (!b)
{
return false;
}
}
return true;
}
private void summon()
{
Vector3[] dots = new Vector3[a * b];
for (int doti = 0; doti < dots.Length; ++doti)
{
dots[doti] = new Vector3(doti % a * space, doti / a * -space, 0);
}
List<Vector3> paths = new List<Vector3>();
bool[] canMove = new bool[8];
int[] modeMove = { 0, 0, 0, -1, 1, 0, 0, 0 };
for (int modei = 0, step = (int)a + 1; modei < 3; ++modei)
{
modeMove[modei] = -step;
modeMove[modeMove.Length - 1 - modei] = step--;
}
bool[] dotMove = new bool[a + b];
int pos = Random.Range(0, (int)(a * b));
int mode = 0;
bool LastDraw = 1 == Random.Range(0, 2);
paths.Add(dots[pos]);
while (!check(dotMove) || LastDraw)
{
canMove[1] = (0 != pos / a);
canMove[3] = (0 != pos % a);
canMove[4] = ((a - 1) != pos % a);
canMove[6] = ((b - 1) != pos / a);
canMove[0] = (canMove[1] && canMove[3]);
canMove[2] = (canMove[1] && canMove[4]);
canMove[5] = (canMove[6] && canMove[3]);
canMove[7] = (canMove[6] && canMove[4]);
if (check(dotMove))
{
canMove[7 - mode] = false;
LastDraw = false;
}
dotMove[pos % a] = true;
dotMove[a + pos / a] = true;
mode = Random.Range(0, 8);
while (!canMove[mode])
{
mode = Random.Range(0, 8);
}
pos += modeMove[mode];
paths.Add(dots[pos]);
}
GetComponent<LineRenderer>().positionCount = paths.Count;
GetComponent<LineRenderer>().SetPositions(paths.ToArray());
}
IEnumerator write()
{
isEnd = false;
yield return new WaitForSeconds(waitTime);
summon();
isEnd = true;
}
void Start()
{
a = (0 == a ? 2 : a + 1);
b = (0 == a ? 3 : b + 1);
space = (0 == space ? 0.809f : space);
if (null == GetComponent<LineRenderer>())
{
transform.AddComponent<LineRenderer>();
GetComponent<LineRenderer>().material = lineMaterial;
GetComponent<LineRenderer>().startWidth = GetComponent<LineRenderer>().endWidth = lineWidth;
}
GetComponent<LineRenderer>().useWorldSpace = false;
summon();
}
void Update()
{
if (!summonOneShot && isEnd)
{
StartCoroutine(write());
}
}
}
后言
刚才看到的这些符号,如果用粒子系统搭配的话,你的游戏就能更好。但是,实际上,你却很难在unity原生的粒子系统上实现这个事情。因此,就得要一个自创的粒子系统,下篇博文教你如何自创粒子系统。