跳到主要内容

字符串操作

简介

教程说明

大家好,这真是一个宁静的夜晚, 至少在编写本教程时是这样的. 所以,接下来我们将深入探讨本文的主要焦点,这篇文章的标题所指明的,将会专注于“字符串操作”在 PAWN 中的应用。我们将从每个人都应该了解的基础内容逐步深入,直至一些高级的、巧妙且有效的技巧。

什么是字符串格式化?

通常来说,格式化文本是通过操作它来提高其可读性的行为,例如更改字体的系列、颜色、粗细等。

字符串是字符的数组(字母、数字、符号),我们不会具体称其为文本,但在显示时会这样称呼。字符串可以用相同的方法处理,但不幸的是,SA-MP 对 PAWN 的解释并不允许进行太多操作(还没有?也许永远不会)。一般来说,改变颜色是我们所能做到的最大限度。是的,你仍然可以更改/自定义字体,但那是客户端侧的操作。是的,GTA 圣安地列斯母游戏)确实提供了一些额外的字体,但那仅适用于 TextDrawsGameText。这确实在文本展示方面存在一些限制,但嘿,已经超过十年了,我们还是很好地生存了下来。

字符串声明

如我之前所说,字符串基本上是字符数组,因此它们的用法与数组相同。因此,就像我们创建一个数组一样,我们也可以按照以下格式来创建字符串:string_name[string_size]

:::信息

string_name:字符数组的名称(例如 string, str, message, text...等等,只要它是一个有效的变量名(以字母或下划线开头))。

string_size:该字符串可以包含的最大字符数。

:::

// 声明一个 5 个字符的字符串
new str_1[5];

// 声明一个 100 个字符的字符串
new str_2[100];

您还可以预先定义一个常量值,以便多次使用它们作为字符串大小.

// 声明一个常量
#define STRING_SIZE 20

// 以 STRING_SIZE 值的大小声明字符串
new str_3[STRING_SIZE];

:::注意

在编译时,编译器将把所有出现的 STRING_SIZE 替换为值 20,这种方法在大多数情况下既节省时间又更具可读性。请记住,分配给 STRING_SIZE 常量的值必须是整数,否则将会导致编译错误。

:::

除了预定义常量之外,你还可以执行基本运算,不过如果使用取模运算符(%)会导致编译错误。你仍然可以进行除法计算(/),但请注意,除以 0 会触发错误。这里的一个额外好处是,所有的浮点结果都会自动为你四舍五入。

// 声明一个常量
#define STRING_SIZE 26

// 使用 STRING_SIZE 常量声明字符串并进行一些计算
new
str_4[STRING_SIZE + 4],
str_5[STRING_SIZE - 6],
str_6[STRING_SIZE * 2],
str_7[9 / 3];

从理论上讲,你可以创建大得离谱的数组, 但 SA-MP 对你可以处理的字符串长度设置了一些限制,根据你正在处理的内容,它限制了你通常可以输出的字符数量。

长度限制

SA-MP 限制了可以存储在单个字符串中的字符数,并防止脚本编写者在处理文本时过度操作。幸运的是,这并不像看起来那么糟糕,下面的列表列出了其中的一些限制:

文本输入你在聊天中输入的文本。128
文本输出在客户端屏幕上输出的文本。144
玩家昵称玩家昵称/用户名。24
Textdraw 字符串顾名思义,表示 Textdraw 的字符串。1024
对话框信息显示在 DIALOG_STYLE_MSGBOXDIALOG_STYLE_INPUTDIALOG_STYLE_PASSWORD 类型对话框中的文本。4096
对话框标题对话框顶部的标题/标题栏。64
对话框输入DIALOG_STYLE_INPUTDIALOG_STYLE_PASSWORD 中的输入框。128
对话框列DIALOG_STYLE_TABLIST_HEADERDIALOG_STYLE_TABLIST 每列中的字符数。128
对话框行DIALOG_STYLE_TABLIST_HEADERDIALOG_STYLE_TABLISTDIALOG_STYLE_LIST 每列中的字符数。256
聊天气泡显示在玩家姓名标签上方的聊天气泡。144
菜单标题GTA 圣安地列斯原生菜单(通常用于商店)的标题。31
菜单项GTA 圣安地列斯原生菜单(通常用于商店)的项目/行。31

如果这些限制被超越,可能会出现一些不便,甚至在某些情况下可能会导致服务器崩溃/冻结(例如,过长的 Textdraw 字符串)。在其他情况下,文本会被截断,比如菜单标题(如果达到 32 个字符,它会截断回 30 个字符)和项目。

除了字符串的严格限制外,还有许多其他限制涉及不同的内容,你可以在这里查看完整列表。

赋值

给字符串赋值可以通过多种方法完成,有些方法是在创建时赋值,有些则是在创建后赋值。有些人使用循环,有些人使用函数,当然也有人手动逐个赋值。没有一个绝对正确或错误的方法,在某些情况下,一些方法可能比其他方法更有效,但最终重要的是性能、优化和可读性。

在大多数情况下,你会希望在创建字符串时就给它赋予一个默认值,你可以通过如下简单方式实现;

new
message_1[6] = "Hello",
message_2[] = "这是另一条消息";

请确保字符串的大小大于您为其分配的字符数,如果字符串的大小小于或等于该字符数,将导致编译错误, 两个方括号[]之间的留空 (就像 message_2 那样), 会自动赋予该数组大小等于您指定的文本大小+1, 在上面的例子中, 7 + 1 = 8, 为什么加1? 因为它为空字符预留了一个位置 (又名 空终止符), 单词 “Hello” 有 5 个字符, 因此要将其存储在字符串中, 应该有 6 个单元格, 其中 5 个单元格用于存储单词的字符, 1个单元格用于存储空字符.

首先,我们定义一个新数组,你可以决定它的大小,也可以留空让编译器填充,两种方法都可以,我们将在数组中填充字符,创建字符串“Hello”.

// 在字符串声明中包含字符串的大小,否则将无法工作
new message_3[6];

message_3[0] = 'H';
message_3[1] = 'e';
message_3[2] = 'l';
message_3[3] = 'l';
message_3[4] = 'o';
message_3[5] = '\0';

在这里,我们为 message_3 数组的每个槽分配了一个字符,如果你声明了一个没有明确大小的字符串,这样的做法是行不通的,请注意,要表示一个字符,它应该写在两个单引号(')之间,另外,请注意我们是如何从槽 0 开始的,这是很自然的、 考虑到我曾强调字符串是一个字符数组,这意味着第一个槽总是 0,最后一个槽是其大小减去 1(空字符不计算在内),在本例中最后一个槽是 4,从 0 到 4,共 5 个字符,第 6 个是空终止符,稍后再详述.

您也可以为字符串分配数字,这些数字将被视为 ASCII 代码(一种用数字表示字符的系统,它涵盖了从 0 到 127 的 128 个字符,更多信息在此), 同样的信息“Hello”可以使用 ASCII 代码分配,如下所示;

new message_4[6];

message_4[0] = 72; // ASCII 代表大写字母 h, “H”
message_4[1] = 101; // ASCII 代表 “e”
message_4[2] = 108; // ASCII 代表 “l”
message_4[3] = 108; // ASCII 代表 “l”
message_4[4] = 111; // ASCII 代表 “o”
message_4[5] = 0; // ASCII 代表空终止符

是的,您可以用这些代码进行数字运算,就像用普通数字运算一样,毕竟机器只是将字符视为数字而已.

new message_5[1];
message_5[0] = 65 + 1;

如果你输出 message_5[0],你会得到 B,奇怪吗?其实并不,因为你可以执行其他基本操作(减法、乘法、除法,甚至是取模运算),浮点数也会自动四舍五入,让我们看看这是如何工作的。

你有 65 + 1,返回值是 66,查阅 ASCII 表,你会发现 66 是字符“大写字母 B”的数字表示。所以,上面的代码片段基本上等同于:message_5[0] = 'B'

参考这张 ASCII 表

你还可以在多个字符之间或字符和数字的组合之间执行相同的操作,以下是一些示例:

new message_6[3];

message_6[0] = 'B' - 1; // 即 66 - 1,返回 65,即“大写字母 A”的数字表示
message_6[1] = 'z' - '&'; // 即 122 - 38,返回 84,即“大写字母 T”的数字表示
message_6[2] = '0' + '1'; // 即 48 + 49,返回“小写字母 a”的数字表示,请注意 '0' 和 '1' 不是数字 0 和 1,而是字符

如果你从未了解过 ASCII 系统,可能会感到困惑. 但只需多加练习,理解其工作原理是非常有帮助的.
ASCII 码不仅限于十进制数字,你还可以以相同的方式使用十六进制或二进制数字

new numString[4];

numString[0] = 0x50; // 80 的十六进制数,即大写字母“P”
numString[1] = 0b1000001; // 65 的二进制数,即大写字母“A”
numString[2] = 0b1010111; // 87 的二进制数,即大写字母“W”
numString[3] = 0x4E; // 78 的十六进制数,即大写字母“N”

现在我们看看其他的内容,通过循环赋值,这与通过循环填充数组的方式完全相同,你可以使用各种循环方法,过程如下;

// 让我们用大写字母填充这个字符串
new message_7[26];

// for 循环
for (new i = 0; i < 26; i++)
message_7[i] = 'A' + i;

// while 循环
while (i++ < 'Z')
message_7[i - 'A'] = i;

// do-while 循环
new j = 'A';

do {
message_7[j - 'A'] = j;
}
while (j++ < 'Z');

// 你甚至可以使用 goto 来模拟循环,但不推荐这样做

它们三个都会输出相同的字符串,即 ABCDEFGHIJKLMNOPQRSTUVWXYZ

如果你觉得上面的循环有些难以理解,建议你深入了解一下循环的工作原理,更多内容可以在这里这里找到。注意我在某些逻辑条件中使用了字符,例如 j++ < 'Z' 这很容易被翻译为 j++ < 90,再一次强调,字符被当作数字处理,不要感到奇怪,你可以随时查阅 ASCII 表。

假设你想用一个特定字符填充一个字符串(例如“AAAAAA”、“TTTTTT”、“vvvvvv”、“666”(不,这不是巧合)),大多数脚本编写者可能会想到手动硬编码,但是对于长字符串呢?当然你可以使用循环,但如果我告诉你有一种更高效的方式呢?就像你用相同的值填充数组一样,你也可以对字符串进行相同的操作。

new message_8[100] = {'J', ...};

上面的代码声明了一个名为 message_8 的新字符串,具有 100 个单元格(范围从 0 到 99),并为每个位置赋予值 'J',这当然可以作为字符 J 或根据 ASCII 系统表示的数字 74 使用。

你还可以使用这种方法将字符串填充为基于区间的字符,看看上面从 AZ 的大写字母的例子吧?让我们使用这种方法创建相同的字符串。

new message_9[26] = {'A', 'B', ...};

这有多简单?!这种方法不仅更优化且易于阅读,还提供与上述使用循环方法的 3 个示例相同的结果。那么它到底是如何工作的呢?我们为字符串提供了初始值 'A''B',它们分别是 6566,编译器计算两者之间的间隔值(在本例中为 1),并根据该间隔值继续填充空单元格,直到填满整个数组。你可以放入任意多个初始值,但编译器只会考虑最后两个值之间的间隔,并以此为基础进行填充。请记住,初始值被视为 ASCII 代码,因此,尝试使用此方法在字符串中输出数字间隔将导致一些不便,假设你声明了如下的随机字符串;

new rand_str[5] = {'1', '5', ...};

理想情况下,这应该输出 151520(更具体地说是 "1 5 15 20"),但实际上输出了 159=A,这实际上是正确的输出。为什么?因为记住,这是 ASCII 码,'1' 是 49,'5' 是 53,两者之间的间隔是 4 (53 - 49)。字符串接受 5 个字符,我们在包含初始值时已经占用了两个单元格,因此剩下的 3 个空单元格必须按照 4 的间隔填充。所以输出结果如下 [ 49 | 53 | 57 | 61 | 65 ]。现在将每个数字值替换为其 ASCII 代码对应值。[ '1' | '5' | '9' | '=' | 'A'],现在更容易理解了吧?

空字符终止符

在本教程的早期部分我提到了这个概念,希望当时不会太让人困惑,但即使有些困惑,现在让我们把这些困惑一扫而空吧。你不必担心,这其实并不难,也不是什么高级概念,只是你应该知道的一个基本事实。我会尽量简短,但如果你想更深入地了解这一点,可以访问这篇文章

空字符终止符(又称空字符)是所有字符串中存在的一个字符,其作用是表示字符串的结束。你可以把它想象成一个句号 (.),任何出现在这个字符之后的内容都不会被计算在内,并且会被完全忽略。你无法使用键盘输入它,但你可以在编码时引用它的值。它在 ASCII 表中存在,被称为 NUL,用数字 0 表示。

pawn 语言中,你可以将其输入为其数值,或作为字符 '\0'。反斜杠在这里作为转义字符,它的作用是告诉机器该字符是值为 0 的空字符,而不是值为 48 的字符 '0'。

pawn 语言中,有一个符号 EOS,它是 End Of String(字符串结束)的缩写,这是空字符终止符的一个预定义宏。你可以通过多种不同的方式设置空字符终止符;

message_9[0] = 0;
message_9[0] = '\0';
message_9[0] = 0b; // 二进制的十进制数 0
message_9[0] = 0x00; // 以十六进制表示的十进制数字 0
message_9[0] = _:0.0; // 对于浮动数字 0.0,我们必须在其前缀上'_',以避免编译错误
message_9[0] = false;
message_9[0] = EOS;

正如我在教程前面所说的,你可以忽略分配空字符,但它总是在空单元格中存在。当你声明一个新字符串时,所有单元格都会自动被空字符占据。因此,例如,如果我声明这个字符串 text[3],它的所有单元格默认都会被分配值 0,这里是该字符串内容的简单可视化表示:

单元格012
ASCII 码000
字符'\0''\0''\0'

下面是另一个预填充字符串的例子。

new text_1[8] = "Hello";

以下是每个单元格的字符串内容;

单元格01234567
ASCII 码72101108108111000
字符'H''e''l''l''o''\0''\0''\0'

例如,如果您想删除该字符串的内容,只需使用以下三个示例中的一个就可以了;

text_1[0] = 0;
text_1[0] = EOS;
text_1[0] = '\0';

将字符串通过 X-Ray 扫描会打印出以下结果;

单元格01234567
ASCII 码0101108108111000
字符'\0''e''l''l''o''\0''\0''\0'

如果尝试输出该字符串,槽 0 之后的所有内容都将被忽略,并被标记为空字符串,甚至 strlen 函数也将返回 0,因为 strlen 依赖于空字符的位置来获取字符串的长度.

字符串操作功能

在处理多文本块时,pawn 可以满足您的需求,它提供了一些非常基本的功能,可以高效地完成工作.

以下是一些本地支持的函数 (摘自 string.inc);

native strlen(const string[]);
native strpack(dest[], const source[], maxlength=sizeof dest);
native strunpack(dest[], const source[], maxlength=sizeof dest);
native strcat(dest[], const source[], maxlength=sizeof dest);
native strmid(dest[], const source[], start, end, maxlength=sizeof dest);
native bool: strins(string[], const substr[], pos, maxlength=sizeof string);
native bool: strdel(string[], start, end);
native strcmp(const string1[], const string2[], bool:ignorecase=false, length=cellmax);
native strfind(const string[], const sub[], bool:ignorecase=false, pos=0);
native strval(const string[]);
native valstr(dest[], value, bool:pack=false);
native bool: ispacked(const string[]);
native uudecode(dest[], const source[], maxlength=sizeof dest);
native uuencode(dest[], const source[], numbytes, maxlength=sizeof dest);
native memcpy(dest[], const source[], index=0, numbytes, maxlength=sizeof dest);

我们将仔细看看其中一些更常用的函数。

  • strlen 函数(这个函数与 sizeof 完全不同),它以一个字符串作为参数,并返回该字符串的长度(即字符的数量)。但要注意的是,它的工作方式有点复杂。我之前在教程中提到过,这个函数依赖于空字符的位置来确定字符串的长度,因此在空字符之后的任何其他有效非空字符都不会被计算在内。一旦遇到第一个空字符,函数就会返回从开头到该空字符的单元格数量。

  • strcat 用于连接字符串,它需要三个参数。

    new str_dest[12] = "Hello", str_source[7] = " World";
    strcat(str_dest,str_source);

如果我们输出 str_dest,它将显示 Hello World,这两个字符串被添加到一起,结果存储在 str_dest 中,“Hello” + “ World” = “Hello World”,注意我们在第二个字符串中包含了一个空格,是的,空格本身也是一个字符,根据 ASCII 表,它的值是 32。如果我们没有添加空格,结果字符串将是 HelloWorld

  • strval 函数将字符串转换为数字,例如,以下字符串 "2017" 将被转换为数字 2017,这对有符号和无符号数字都有效。如果字符串中没有数字字符,函数将返回 0。如果字符串中有数字字符但以非数字字符开头,函数也会返回 0。如果字符串以数字字符开头但同时包含非数字字符,数字字符仍会被检索并转换,以下是一些使用示例:

    strval("2018"); // 返回 “2018”。
    strval("-56"); // 返回 “-56”。
    strval("17.39"); // 返回 “17”,浮点数 17.39 被自动取整为 17。
    strval("这没有数字"); // 返回 “0”。
    strval("6 颗星"); // 返回 “6”。
    strval("起航, 2018"); // 返回 “0”。
    strval("2017 已结束, 欢迎来到 2018"); // 返回 “2017”。

:::提示

你可以下载许多与字符串操作有关的社区库, 但我想不出比 strlib 更好的包含库了.

:::

format 函数

这可能是社区中最常用的字符串相关函数,非常简单且用户友好,它的作用是格式化文本块并将它们拼接在一起。它可以在各种情况下实现,例如将变量和字符串连接在一起、嵌入颜色、添加换行符等等.

format(output[], len, const format[], {Float, _}:...)

format 函数的参数包括输出数组、其大小(单元格的数量)、格式化字符串(这可以预先存储在另一个数组中,也可以直接从函数内部分配),最后是一些可选的参数,这些可以是不同类型的变量。让我们使用这个函数给一个空字符串赋值。

new formatMsg[6];
format(formatMsg, 6, "Hello");

formatMsg 的输出的结果是 Hello,请记住,这种赋值字符串的方式不佳,主要是因为其速度,通常有更好的方法来实现这一点,我们在教程的早期阶段已经讨论过其中一些方法。

记住要始终提供正确的数组大小,否则它虽然会工作,但会带来一些和预期效果不符的行为,format 函数会溢出你的数组大小,相信我,你不希望这种情况发生。如果你不想每次使用这个函数时都麻烦地提供正确的字符串大小,你可以简单地使用 sizeof 函数(严格来说,它不是一个函数,而是一个编译器指令)。我们之前见过一个名为 strlen 的函数,它返回字符串的字符数(不包括并在空字符处停止),但这个函数返回的是数组的大小,换句话说,就是这个数组的单元格数量,无论它们是否被有效字符填充,在这个例子中为 6。

new formatMsg[6];
format(formatMsg, sizeof(formatMsg), "Hello");

文本必须始终包含在双引号中,然而,还有一种不常用的文本输入方式,它使用井号 # 符号,工作方式如下:

new formatMsg[6];
format(formatMsg, sizeof(formatMsg), #Hello);

它支持空格、转义字符,甚至可以混合使用双引号和井号:

new formatMsg[6];
format(formatMsg, sizeof(formatMsg), "Hello "#World);

上面的代码将输出 Hello World。这种输入字符串的方法更常用于预定义常量。让我们来看一个例子,其中使用了两个不同的预定义常量,一个是整数 2017,另一个是字符串 "2018"

#define THIS_YEAR 2018 // 这个常量的值是一个整数
#define NEW_YEAR "2019" // 这个常量的值是一个字符串

new formatMsg[23];
format(formatMsg, sizeof(formatMsg), "This is "#THIS_YEAR", not"NEW_YEAR);

这将输出 This is 2018, not 2019。之所以强调两个常量的类型不同,是因为使用井号 #。如果值不是字符串,你必须用井号 #THIS_YEAR 前缀,以便它被当作 "2018" 处理,否则会产生编译错误。对于字符串值,你可以选择加上或省略井号,因为它们都能正常工作(NEW_YEAR#NEW_YEAR 是一样的)。这种方法只适用于从预定义常量中获取值,对于常规变量或数组/字符串,应该使用占位符,稍后会详细介绍。

你还可以在双引号旁边排布尽可能多的双引号,虽然这没有意义,因为更自然的方式是将句子写在一对双引号中。以下是用两种概念编写的相同句子的示例:

new formatMsg[29];

// 一组单引号
format(formatMsg, sizeof(formatMsg), "This is reality...or is it?!");

// 多组双引号
format(formatMsg, sizeof(formatMsg), "This is reality""...""or is it?!");

两者将输出相同的句子, This is reality... or is it?!.

优化技巧

现在,我们已经了解了一些关于字符串声明、操作等基本知识。很多人可能会直接开始实践,而忽视了社区遵循的一些通用准则。如果更多人关心可读性、优化和性能,世界会变得更好。一个编译正常的代码不一定工作良好,大多数错误来自那些我们忽视的小细节,或者由于它们与其他系统的交互不友好。编写良好的代码能够经受时间的考验,怎么做到呢?你可以随时回来调试、修复、审查它,优化将直接反映在性能上,始终尽可能发挥机器的最佳性能,优化的代码是关键。

首先必须提到的,也是我个人感到困扰的,是看到如何创建了大量的字符串,但几乎没有使用声明的所有单元格,只声明你将使用的字符串大小,额外的单元格只会占用更多的内存。让我们来看一个所谓的不优化字符串声明方式。

new badString[100];
badString ="Hello :)";

我们声明了一个包含 100 个单元格 的字符串,1 个单元格 占用 4 字节,让我们做一些基本的数学运算,100 * 4 = 400 字节,这大约是 0.0004 兆字节,对于今天的标准来说,这确实不算什么,但假设在一个庞大的脚本中,你显然会使用多个字符串,6070,甚至 100 个字符串?(可能更多),这些微小的数字会叠加在一起,导致一个更大的总数,并且可能会给你带来严重的问题。我敢保证,当你与那些大小是五倍以上的字符串相比时,我们声明的字符串还远远算不上愚蠢。

我更常见到的一种情况,虽然有些陈词滥调,但却是使用神秘的 256 长字符串,为什么呢?为什么这样做呢?

请记住,SA-MP 在处理字符串时的限制,256 长 的字符串到底有什么用?你要用这么长的字符串做什么(除了格式化对话框/文本绘制字符串)?最大字符串输入长度是 128 字符,那正好是这个长度的一半,512 字节 就这样浪费了,怎么回事?你打算用它来输出,而不是输入?即便如此,仍然太大了,输出字符串不应超过 144 字符,你明白我的意思了吗?让我们试着纠正这个错误,我们有这样一句话,“Good string”,它包含 11 个字符(空格也算作一个字符)+ 1 作为空终止符(一定要记住这个家伙),这总共是 12 个字符。

new goodString[12];
goodString="Good string";

看看我们如何节省了内存?仅仅 48 字节,没有额外的负担,避免了后续可能出现的问题,感觉好多了。

但是,如果我告诉你,你可以得到一个更优化的代码,那就是 打包字符串packed strings)?没错,你听说过打包字符串吗?一个字符串通常由多个单元格组成,正如我们之前所说的,每个单元格代表 4 字节,所以字符串由多个 4 字节 的集合组成。单个字符占用 1 字节,而每个单元格仅允许存储一个字符,这意味着,每个单元格中有 3 字节被浪费掉了。

new upkString[5];
upkString = "pawn";

上面的字符串占用了 5 个单元格(大约 20 字节),但可以缩减到仅 8 字节,即仅 2 个单元格.

new pkString_1[5 char];

pkString_1 = !"pawn";
// 或者
pkString_1 = !#pawn;

简单来说,这就是它的工作方式:你声明一个字符串,大小是它通常需要的大小(当然包括空字符),然后在其后添加 char 关键字。每个字符将以字节而不是单元格存储,这意味着每个单元格将存储 4 个字符。记住,当为打包字符串赋值时,要在前面加上感叹号 !,不过这不适用于单个字符.

这是 upkString 内容的近似视觉表示;

单元格01234
字节0 . 1 . 2 . 30 . 1 . 2 . 30 . 1 . 2 . 30 . 1 . 2 . 30 . 1 . 2 . 3
字符\0 . \0 . \0 . p\0 . \0 . \0 . a\0 . \0 . \0 . w\0 . \0 . \0 . n\0 . \0 . \0 . \0

And this is what pkString_1 would be like in the second example;

单元格01
字节0 . 1 . 2 . 30 . 1 . 2 . 3
字符p . a . w . n\0 . \0 . \0 . \0

你还可以像下面这样访问打包字符串的索引器;

new pkString_2[5 char];

pkString_2{0} = 'p';
pkString_2{1} = 97; // 字符 "a" 的 ASCII 码。
pkString_2{2} = 0b1110111; // 二进制的 199 转换为十进制为字符 "w"。
pkString_2{3} = 0x6E; // 十六进制的 110 转换为十进制为字符 "n"。
pkString_2{4} = EOS; // EOS(字符串结尾),值为 0,即 \0(NUL)的 ASCII 码,空字符。

结果将与此情况下的 pkString_1 相同。正如你所看到的,ASCII 码仍然被考虑在内。注意,当访问打包字符串的索引器时,我们使用 大括号 而不是 方括号。这意味着我们正在索引字节本身,而不是单元格

:::信息

尽管打包字符串在节省内存方面很有效,但 SA-MP 对 pawn 的实现并不完全支持打包字符串,但你仍然可以在不常使用的字符串/数组中使用它们.

:::

字符串输出

控制台

print

以下的函数可能是不仅仅在 Pawn 语言中,而是在许多其他编程语言中最基本的函数之一,它仅接受一个参数并将其输出到控制台.

print("Hello world");
Hello world

你还可以传递预先声明的字符串或预定义的常量,并将它们合并在一起,或者像我们以前使用 format 函数时那样使用井号 #。但请记住,这不包括多个参数,我们只能传递一个且仅有一个参数。

#define HAPPY_STRING "I'm happy today" // 字符串常量.
#define NEW_YEAR 2019 // 整数常量.
new stylishMsg[12] = "I'm stylish";

print(HAPPY_STRING);
print(stylishMsg);
print(#2019 is beyond the horizon);
print("I'm excited for "#NEW_YEAR);
print("What ""about"" you""?");
I'm happy today
I'm stylish
2019 is beyond the horizon
I'm excited for 2019
What about you?

注意我们在这里使用了与 format 函数相同的方式使用井号 #,如果值是整数,你需要在前面加上 #,这样它就会被视为字符串。

还要记住,print 函数确实支持压缩字符串,但只接受字符串类型的变量(字符数组),传递任何不是数组的内容或字符串(无论是双引号中的字符串还是由井号符号前缀的字符串)都会导致编译错误,因此,以下操作都无法实现:

// 情况 1
new _charA = 'A';
print(_charA);

// 情况 2
new _charB = 66;
print(_charB);

// 情况 3
print('A');

// 情况 4
print(66);

看看我们将如何修复它;

// 情况 1
new _charA[2] = "A";
print(_charA);

我们将单引号更改为双引号,并为数组提供两个单元格,一个用于字符 A,另一个用于空字符终止符,因为在双引号之间的任何内容都是字符串,输出结果为 A

// 情况 2
new _charB[2] = 66;
print(_charB);

我们将 _charB 更改为一个包含一个单元格的数组,并将标记为 0 的单元格设置为值 66,根据 ASCII 表,这转换为 B,输出结果为 B,我们为空字符终止符保留了一个额外的单元格。

// 情况 3
print("A");

没有太多可说的,只需将单引号更改为一对双引号.

至于第四种情况,当使用 print 函数时,我们能做的不多,但可以通过使用另一个类似的函数来简单地解决这个问题,称为...

printf

这是 “打印格式化(print formatted)”, 简单来说,这是前一个 print 函数的扩展版本,更具体地说,它像是 format 函数和 print 函数的组合,这意味着它也在服务器控制台上打印字符,但具有格式化输出文本的好处.

print 不同,printf 接受多个参数,并且可以是不同的类型,但它不支持压缩字符串。为了扩展其功能,我们使用这些称为“格式说明符(format specifiers)”的序列,稍后会详细介绍。输出超过 1024 个字符会导致服务器崩溃,请注意这一点

#define RANDOM_STRING "Vsauce"
#define RANDOM_NUMBER 2018

printf("Hey "RANDOM_STRING", Micheal here! #"#RANDOM_NUMBER);

请注意,我们类似于 printformat 函数,将这些字符串嵌套在一起,输出如下:

Hey Vsauce, Micheal here! #2018

正如我之前所说,当使用 格式说明符 时,printf 函数真正展现了它的优势。这是它的独特之处和区别所在。您可以附加任意数量的变量,并轻松输出简单和复杂的字符串。我们将在稍后介绍这些说明符时深入探讨这一点。

客户端消息

除了可以打印在服务器控制台上的其他调试文本外,还有显示在客户端屏幕上的消息,这些消息显示在聊天部分,它们也可以像之前的文本一样进行格式化,但同时还支持颜色嵌入,如果正确使用,会使文本的呈现效果非常出色。

请记住,SA-MP 对显示字符串的限制也适用于这种类型的消息,与之前的消息一样,字符数限制为小于 144 字符,否则消息将无法发送,有时甚至会导致某些命令崩溃。

有两个函数可以在客户端屏幕上原生地打印文本,它们之间的唯一区别是作用范围。第一个函数接受三个参数:要在其屏幕上打印文本的玩家 ID、文本的颜色,以及文本本身。

SendClientMessage(playerid, color, const message[])

比方说,您想向 ID 1 的玩家发送一条文本,告诉他们 “Hello there!”;

SendClientMessage(1, -1, "Hello there!");

很简单,就像这样,ID 1 的玩家将收到一段文字 Hello there!, -1 是颜色参数, 在本例中是白色, 关于颜色的内容稍后详述.

当然,你也可以传递数组、格式化字符串......等等。正如我们在其他函数中看到的,你可以使用符号 #

#define STRING_MSG "today"
new mornMsg[] = "Hello!";

SendClientMessage(0, -1, mornMsg);
SendClientMessage(0, -1, "How are you ",STRING_MSG#?);

正如您在上面的示例中看到的,这将向 ID 0 的玩家发送两条白色的信息,第一条信息将显示“Hello!”,第二条信息将显示“How are you today?”,这与其他函数的工作方式非常相似。请记住,预定义的常量整数必须以 # 号作为前缀.

#define NMB_MSG 3
SendClientMessage(3, -1, "It's "#NMB_MSG" PM");

非常直观,文本将发送给 ID 3 的玩家,颜色为白色,内容为“It’s 3 PM”。

现在您知道如何向某人发送消息了,您可以使用相同的方法向所有人发送相同的消息,真的很简单,您可以将函数放在一个遍历所有连接玩家的循环中,并冒险将您的代码公布就结束了,但嘿,实际上已经有一个原生函数可以完成完全相同的事情,唯一的不同在于它们的语法略有变化。

SendClientMessageToAll(color, const message[]);

也是非常直观,从它的名字就可以看出,现在让我们给服务器上的每个人发送一条问候信息.

SendClientMessageToAll(-1, "Hello everyone!");

就像那样,但尽量不要超过 144 个字符的限制。

Textdraws

这是 SA-MP 最强大的功能之一,只需释放您的想象力,textdraws 基本上是可以显示在客户端屏幕上的图形形状/文本/精灵/预览模型等等,它们使 UI 特别生动和互动(当然有一定的限制)。但是,这里也有一些限制,例如,您不能显示超过 1024 个字符的字符串,老实说,这已经足够了。即使它们功能广泛,但可以显示的字符串在格式化方面也很贫乏,您不能像使用其他输出函数那样做很多事情,在这方面感觉有点狭隘,但它确实通过其他令人兴奋的内容弥补了格式化的缺失,更多关于 textdraws 的内容请参考 这里

Dialogs

Dialogs 可以被视为“消息框”,当然,它们有不同的类型,接受几种不同的输入,最重要的是,它们接受所有普通字符串的格式化方式,这使得它们比 textdraw 更容易使用。它们也有一些限制,比如字符串大小以及只能在客户端屏幕上同步显示,SA-MP 只提供了一个原生函数来处理 dialogs,老实说,这将是您的最后关注点,因为这个唯一的函数完成了它的工作,并且效率很高,更多关于 dialogs 的内容请参考 这里

颜色解释

客户端消息和对话框

RGBA

RGBA红绿蓝透明度的简称)是 RGB 模型的简单使用,额外添加了一个通道,即 alpha 通道,基本上是一种通过混合红色、绿色、蓝色和 alpha(不透明度)的变体来数字表示颜色的形式,更多内容请参考 这里

在 SA-MP 的 pawn 实现中,我们使用十六进制数字来表示这些颜色空间,红色、绿色、蓝色和 alpha 每个用 2 位表示,总共 8 位长的十六进制数字,例如;(FF0000FF = 红色)、(00FF00FF = 绿色)、(0000FFFF = 蓝色)、(000000FF = 黑色)、(FFFFFFFF = 白色),以下是这种符号表示的更清晰的可视化:

FF FF FF FF

许多编程/脚本语言的十六进制数字前缀是数字符号 #,而在 pawn 中,我们使用 0x 作为前缀,所以以下十六进制数字 8060C1FF,变成 0x8060C1FF

当然,我们也可以使用十进制数字来表示颜色,但使用十六进制表示会更清晰,因为它比两者中更具可读性,以下是一个例子;

// 用十进制数字表示白色
SendClientMessageToAll(-1, "Hello everyone!");

// 用十六进制数字表示白色
SendClientMessageToAll(0xFFFFFFFF, "Hello everyone!");

// 将向所有人发送一条白色的客户端信息

请记住,将所有位(bits)指定为相同的值将导致灰色深浅不一,将 alpha 通道指定为 0 将使文本不可见

:::提示

同时使用多种颜色的文本格式也是可以的,但为此我们嵌入了更简单的 RGB 符号

:::

RGB

这与 RGBA 颜色空间非常相似,但没有 alpha 通道,只是红、绿、蓝的混合,以 6 位的十六进制数字表示,在 pawn 中,这种表示法通常用于将颜色嵌入到文本中,只需将您的 6 位十六进制数字用一对大括号括起来,就可以开始使用了,例如: ({FF0000} = 红色)、 ({00FF00} = 绿色)、 ({0000FF} = 蓝色)、 ({000000} = 黑色)、 ({FFFFFF} = 白色),这里是这种表示法的更清晰的可视化:{FFFFFF}。让我们看一个简单的示例:

SendClientMessageToAll(0x00FF00FF, "I'm green{000000}, and {FF0000}I'm red");

这将向所有人发送以下消息(I'm green, and I'm red):

我为绿色 , 并且 我为红色

请记住,十六进制表示法不区分大小写,因此输入 0xFFC0E1FF 与输入 0xfFC0e1Ff 是一样的,嵌入的颜色也是如此,{401C15}{401c15} 是一样的。

有时候,处理颜色可能会很麻烦,记住所有这些长十六进制数字并不容易。您应该始终有一个参考可以回去查看,网上有很多颜色选择工具,您可以简单地搜索“color picker”,并在成千上万的工具中选择一个。如果您不介意的话,我为您提供一个推荐工具,这是一个简单的工具,我建议在处理颜色时使用它。

人们发现的一个问题是管理工作流程,如果做得对,可以使工作节奏更顺畅,减少处理项目时的痛苦。虽然颜色选择工具非常有帮助,但每次需要选择颜色时来回切换仍然会浪费大量时间,这种挫败感就像披萨上放了菠萝一样令人烦恼。幸运的是,您可以利用预定义的常量,为稍后使用定义您最常用的颜色,这里有一个简单的示例;

#define COLOR_RED 0xFF0000FF
#define COLOR_GREEN 0x00FF4AFF
#define COLOR_BLUE 0x0058FFFF

SendClientMessageToAll(COLOR_RED, "我是红色的文本");
SendClientMessageToAll(COLOR_GREEN, "我是绿色的文本");
SendClientMessageToAll(COLOR_BLUE, "我是蓝色的文本");

The latter can be done on embedded colors too;

#define COL_RED "{FF0000}"
#define COL_GREEN {00FF4A}
#define COL_BLUE "{0058FF}"

SendClientMessageToAll(-1, ""COL_RED"我是红色的文本");
SendClientMessageToAll(-1, "{"COL_GREEN}"我是绿色"COL_BLUE"和蓝色");
ShowPlayerDialog(playerid, 0, DIALOG_STYLE_MSGBOX, "注意", "{"COL_GREEN"}你好! "COL_RED"近况如何?", "关闭", "");

在编译时,所有预定义的常量将被替换为它们的值,因此,"COL_RED"我是红色的文本 变成了 "{FF0000}"我是红色的文本,注意我们使用了两种预定义颜色的方法,RRGGBB{RRGGBB},选择哪种方法取决于个人偏好,个人来说,我觉得定义为 RRGGBB 更清晰,因为使用了大括号,这使得我们可以明显看出我们在嵌入颜色。

这就是对话框和客户端消息字符串中颜色嵌入的总体方法,在客户端消息、对话框、3D 文本标签、对象材质文本和车辆号牌中使用颜色是可能的,但 SA-MP 也有文本绘制(textdraws)和游戏文本(gametexts)功能,然而这些不支持 RGB 表示法,因此添加颜色的方式有所不同。

文本绘制(Textdraws)和游戏文本(Gametexts)

如上所述,RGB 表示法不被支持,但幸运的是,我们有其他方法来解决这个问题,对于文本绘制,您可以使用本地函数 TextDrawColor 来更改文本绘制的颜色,但这与文本绘制的 RGBA 颜色空间对于客户端消息和对话框是一样的,颜色不能被嵌入。为此,我们使用特殊的字符组合来引用颜色和其他一些符号,您可以在 这里 查看它们。

~r~红色
~g~绿色
~b~蓝色
~w~ or ~s~白色
~p~紫色
~l~黑色
~y~黄色

因此,嵌入颜色可以这样做: ~w~Hello this is ~b~blue ~w~and this is ~r~red

您还可以使用另一种字符组合来玩混色游戏, ~h~, 它会使某种颜色变浅,以下是几个例子:

~r~~h~亮红色
~r~~h~~h~粉红色
~r~~h~~h~~h~暗红色
~r~~h~~hhh~亮粉色
~r~~h~~h~~h~~h~~h~粉色
~g~~h~亮绿色

您可以在这里找到更多相关信息.

转义字符

描述

转义字符是指在某些字符或数字前加上前缀时,会创建出其自身的常量字符,在大多数编程/脚本语言中,如 pawn,反斜杠 \ 被用作转义字符,结合其他字符/数字将产生一个具有特定意义的 转义序列,你可以在 这里 了解更多关于转义字符的内容。

转义序列

转义序列使得在脚本的源代码中表达某些字符变得更容易,下面是 pawn 中使用的转义序列表:

响铃(在服务器机器上)\a\7
退格符\b
转义符\e
换页符\f
换行符\n
回车符\r
水平制表符\t
垂直制表符\v
反斜杠\\
单引号\'
双引号\"
十进制值“ddd”的字符代码\ddd;
十六进制值“hhh”的字符代码\xhhh;

让我们逐一看一下这些序列,毕竟,学习这些内容的最好方式就是进行实践。

  • “响铃”转义序列 - \a

响铃或称为铃声代码(有时也称为铃字符)是一种设备控制代码,最初用于在打字机和电传打字机上响起一个小的电动机械铃,以提醒另一端的操作员,通常是有来电或消息的提示。

在计算机上使用此序列会在后台发出一个铃声/通知声音,这可以在某些创意场景中使用,以通知和/或提醒用户某些活动,表示它的转义序列是 \a(或 \7 表示为十进制代码),启动你的 pawn 文本编辑器,编写以下代码;

print("\a");

执行 samp-server.exe 后,您将听到蜂鸣声通知,您也可以使用十进制代码;

print("This is a beep \7");
  • “退格符”转义序列 - \b

这个转义序列记作 \b,它只是将光标向后移动。大多数人会期望它像典型键盘上的退格键一样工作,但实际上并不完全相同,它只是将打印头向后移动一个位置,而不会删除已经写入的内容。

这个转义序列在 pawn 中的实用性并不高,除非你能巧妙地利用它,下面是它的工作方式。

print("Hello 2018");

这将在控制台中打印 Hello 2018,光标停留在空字符的位置上,更清楚地显示出来,就像这样:

Hello 2018
^

如你所见,光标在字符串的最后一个可见字符后停止,这是正常现象,现在,让我们添加一个退格转义序列;

print("Hello 2018\b");

这样做的结果是;

Hello 2018
^

正如您所看到的,光标正好位于字符串最后一个可见字符的位置,即 8,这与在键盘上切换插入模式是一样的

print("Hello 2018\b9");

如果你猜对了,没错,这将打印 ** 你好,2019**,那么,让我们来看看它是如何工作的:机器将逐个字符地处理字符串,直到它抵达退格转义序列的位置,然后它将向后移动一个位置,这将选择那里的任何字符,在本例中是 8,然后,它将在其位置上插入 9.

Hello 2019
^

只要你的字符串中有回退字符转义序列,光标就会向后移动。

print("Hello 2018\b9\b\b\b");
Hello 2019
^

如果回退字符转义序列的数量超过了第一个字符的位置(是的,数组从0开始,前往 r/programmerhumor 看一些有趣的梗)与光标初始位置之间字符的数量,光标将停留在第一个字符的位置。

print("Hi\b\b\b\b\b\b\b\b\b\b\b\b\b\b");

结果总是这样:

Hi
^
  • “转义符” 转义序列 - \e

ASCII 中其十六进制值为 1B,通常用于常见的非标准代码,以 C 语言为例;像 \z 这样的序列在 C 标准中不是有效的转义序列。C 标准要求诊断此类无效的转义序列(编译器必须打印错误信息)。尽管如此,一些编译器可能会定义额外的转义序列,具有实现定义的语义。例如,\e 转义序列表示转义字符。然而,它没有被添加到 C 标准中,因为在某些字符集里没有有意义的等效项。

  • “换页符” 转义序列 - \f

Form feed 是一个用于分页的 ASCII 代码。它强制打印机弹出当前页,并在另一页的顶部继续打印。通常,它也会导致回车,这在 SA-MP 的调试控制台中没有明显的变化。

  • “换行符” 转义序列 - \n

新行(也称为行结束、行尾 (EOL)、换行或换行符)转义序列是一个 ASCII 代码,以 /n 表示,十进制值为 10,它是常用的,文本编辑器在我们按下键盘上的 Enter 键时会插入这个字符。下面是一个带有换行符的简单消息:

print("Hello, this is line 1\nAnd this is line 2");

这将简单地输出:

Hello, this is line 1
And this is line 2

当然,可以实现多个换行符:

print("H\n\n\ne\n\n\nl\nl\n\no");
H


e


l
l

o

然而,这在处理文件时表现不同,根据你的操作系统,例如,在 Windows 中,换行符通常是 CR (carriage return) + LF (line feed),你可以了解更多不同之处 这里

  • “回车符” 转义序列 - \r

回车是一个 ASCII 代码,通常与换行符一起使用,但它也可以单独作为一种功能,它只是将光标移动到当前行的开头,相当于我们讨论的使用多个回退 (\b) 转义序列的特定情况。让我们看以下示例,如果不使用这个转义序列,我们会得到正常的输出:

print("Hello");
Hello
^

箭头表示光标的位置,位于字符串的最后一个可见字符之后,这就是正常的预期行为,现在让我们将回车符添加到混合中:

print("Hello\r");
Hello
^

光标被移动到行的开头,选择第一个字符 “H”,现在插入任何内容将把 “H” 更改为我们输入的内容,然后移动到下一个字符,同时保持插入模式:

print("Hello\rBo");
Hello
^

正如我们在换行部分看到的,不同操作系统的换行符行为不同,例如,Windows 使用回车符后跟换行符来执行换行,就像经典的打字机一样。

  • “水平制表符” 转义序列 - \t

制表符是我们每天都在使用的,从文本/代码缩进到表格显示,键盘上那个制表符键确实节省了很多时间,以前过多地使用空格是很麻烦的,但这个键可以轻松解决这个问题,它在编程领域中也非常常见,表示为 \t,人们会争论一个制表符值多少空格,大多数人说是 4 个空格,但也有人说是 8 个空格,有人甚至更喜欢用空格代替制表符,但这另当别论,下面是一个简单的例子:

print("Hello\tWorld");
Hello    World

这是另一个带有多个制表符的例子:

print("Hello\t\t\t\t\tWorld");
Hello                    World
  • “垂直制表符” 转义序列 - \v

在早期打字机时代,这个转义序列更为流行,它用于垂直移动到下一行,但现在已经不再使用,它在现代打印机和编程语言中没有明显的用途,pawn 也不例外。

  • “反斜杠” 转义序列 - \*

正如我们所见,反斜杠被视为转义字符,因此每当程序遇到它时,它会将其视为某些转义序列的起始点,而不是独立的字符,因此,要么会导致编译错误(如果没有后续有效字符),要么不会打印它。在 pawn 的情况下,编译器将引发错误(错误 027:无效的字符常量)。幸运的是,我们可以通过转义反斜杠来解决这个问题,方法是在其前面加上另一个反斜杠:

print("Hello \\ World");
Hello \ World
­警告

输出将忽略第一个反斜杠,并打印第二个,因为第一个反斜杠转义了第二个并欺骗程序将其视为原始字符。一个反斜杠只能转义一个字符,因此以下做法会引发编译错误。

print("Hello \\\ World");

将其视为成对的反斜线,每个都会转义后面的一个,因此,其结果应该总是双数的反斜线;

print("Hello \\\\\\ \\ World");
Hello \\\ \ World

你肯定注意到了,转义序列是不会被打印出来的,它们只是作为表达某些事件的指令,如果我们想强制将它们打印出来,可以使用转义字符(\),这样程序就不会将它们视为转义序列了:

print("这是负责制表的转义序列: \\t");

第一个反斜杠转义了第二个反斜杠,然后它被打印出来,然后t 字符被单独保留,因此被视为一个独立字符:

这是负责制表的转义序列: \t
  • “单引号” 转义序列 - \'

在其他语言中,单引号之间的文本被视为一个字符串,这一点在其他语言中得到了很好的应用,以减少单引号之间嵌套时产生的混乱;

print("单引号 '");
// 或者
print("单引号 \'");

无论采用哪种方式,输出结果都是一样的:

单引号: '

我能想到的与此相关的唯一用途是将变量设置为字符 “'”,因此,执行以下操作显然会导致编译错误;

new chr = ''';

这是因为编译器会将第一对单引号视为一个实体,而将第二对单引号视为一个未封闭的引号序列,因此要解决这个问题,我们必须转义中间的单引号;

new chr = ''\';
  • “双引号” 转义序列 - \"

与单引号不同,双引号在嵌套时会产生问题,Pawn 将双引号之间的任何内容都视为一个字符串,因此,如果您想在字符串中输入双引号,程序会感到困惑,因为它不知道每个引号的作用:

print("Hello "world");

编译器一旦发现第一个引号,就会将后面的所有内容视为一个字符串的一部分,并在遇到另一个引号时结束编译过程,因此,编译器会将 "Hello " 视为一个字符串,而将World " 视为填补代码漏洞的无意义内容。要解决这个问题,我们需要转义我们要打印的双引号:

print("Hello \"world");

现在,编译器会将第二个引号视为转义序列,因为它的前缀是转义字符 (\):

Hello "world

为了方便起见,我们再加一个引号吧:

print("Hello \"world\"");
Hello "world"

再简单不过了.

在本节中,我们看到了如何通过在某个字符前加上转义字符 (\\)来表示转义序列,但这只是记录这些值的一种方法,我们还将了解其他两种方法;

  • 带字符代码(十进制代码)的转义序列 - \ddd;

这并不会改变转义序列的任何东西,它只是用十进制 ASCII 代码表达它们的另一种方式。例如,如果你想打印字母 A,但使用十进制表示,你可以像下面这样输入它的十进制 ASCII 代码:

print("\65;");

这不仅限于字母数字字符,还包括其他字符,例如可听的蜂鸣声(\a),它的十进制值为 7,可以根据这种表示法表示为 \7;

分号是可选的,可以省略,但最好使用原始的表示法,其目的是当它在字符串常量中使用时,为转义序列提供一个明确的结束符号。

  • 带字符代码(十六进制代码)的转义序列 - \xhhh;

类似于十进制 ASCII 表示法,我们也可以使用十六进制格式,字符 A 可以写作 \65;\x41;,分号在这里和十进制表示法中都是可选的。

print("\x41;");
A

你可以通过简单地搜索“ASCII 表”找到所有这些值,而且更酷的是,它是免费的。

自定义转义字符

如果你注意到了,我在上一部分中多次提到“转义字符”,而不是简单地称其为“反斜杠”或缩写为 (\)。这是因为转义字符不是绝对的常量字符,而是可以根据需要进行更改的。你可以将其设为 @, ^, $ 等等,默认情况下它是反斜杠,但它的状态由你决定。

为了更改它,我们使用预处理指令 pragma,该指令接受不同的参数,每个参数有其特定的任务,其中有一个负责设置转义字符,我们将重点关注它,即 ctrlchar

#pragma ctrlchar '$'

main()
{
print("Hello $n World");
print("这是一个反斜杠: \\");
print("这是一个美元符号: $$");
}
Hello
World
这是一个反斜杠: \
这是一个美元符号: $

如你所见,换行符现在表示为 $n 而不是 \n,而反斜杠不再被视为转义字符,因此,美元符号需要由另一个美元符号转义。

然而,你不能将其更改为 (-),但任何其他字符都是可以接受的实践。不过,理论上永远不被接受的情况如 #pragma ctrlchar '6',多么滑稽啊,真是个疯狂的家伙。

这一部分与转义序列完全无关,但它用于格式化文本绘制和游戏文本,放在这里比放在其他地方更合适;

~u~向上箭头(灰色)
~d~向下箭头(灰色)
~<~向左箭头(灰色)
~>~向右箭头(灰色)
]显示一个 * 符号(仅在文本样式 3、4 和 5 中)
~k~键盘键映射(例如 ~k~~VEHICLE_TURRETLEFT~~k~~PED_FIREWEAPON~)。查找按键列表请点击此处。

格式说明符

描述

占位符或说明符是以百分号(%)转义的字符,它们表示某些参数的相对位置和输出类型,正如其名称所暗示的那样,它们是“占位符”,它们为稍后将在字符串中替换它们的数据保留了一个位置。有不同类型的说明符,它们甚至遵循一个特定的公式;

%[flags][width][.precision]type

方括号中的属性都是可选的,由用户自行决定是否保留它们。真正定义一个说明符的是众所周知的格式 %type,其中类型部分由一个字符替代,以表示某种输出类型(例如整数、浮点数等)。

占位符仅用于接受参数的函数,因此像 print 这样的函数不会受到影响。对此的一个替代方案是更为高级的 printf 函数。

让我们来看一下可以使用的不同输出类型:

Specifier含义
%i整数 (整数)
%d整数 (整数)
%s字符串
%f浮点数 (Float: tag)
%cASCII 字符
%x十六进制数
%b二进制数
%%字面量 '%'
%q转义文本以适用于 SQLite。(在 0.3.7 R2 中添加)
  • 整数格式说明符 - %i%d

我们将这两个格式说明符放在一起讨论,在 pawn 中,这两个说明符执行完全相同的操作,都是输出整数。尽管 %i 代表整数,而 %d 代表十进制,它们是同义的。

然而,在其他语言中,它们的区别不在于输出,而在于输入。例如在 scanf 函数中,%d 将整数扫描为带符号的十进制,而 %i 默认是十进制,但也允许十六进制 (如果前缀为 0x) 和八进制 (如果前缀为 0)。

这两个说明符的用法如下:

printf("%d is here", 2018);
printf("%d + %i = %i", 5, 6, 5 + 6);
2018 is here
5 + 6 = 11

输出还支持预定义常量、变量和函数.

#define CURRENT_YEAR 2018
new age = 19;

printf("现在是 %d 年", CURRENT_YEAR);
printf("他现在 %d 岁了", age);
printf("1970 年 1 月 1 日午夜之后的秒数: %d", gettime());
现在是 2018 年
他现在 19 岁了
1970 年 1 月 1 日午夜之后的秒数: 1518628594

正如你所看到的,我们传递给 printf 函数的任何值都会被其对应的占位符替换,记住,顺序很重要,你的占位符应与调用中的参数顺序一致,并且始终使用正确的格式说明符,不这样做不会导致错误,但可能会产生一些不想要的结果,不过在某些情况下,这些不想要的结果就是我们想要的。

你认为如果我们尝试使用整数格式说明符来打印浮点数或字符串会发生什么呢?让我们来看看;

printf("%d", 1.12);
printf("%d", "Hello");
printf("%d", 'H');
printf("%d", true);
1066359849
72
72
1

多么奇怪,完全出乎意料,但不一定是无用的,这种行为在许多情况下都被利用。

首先,让我们看看为什么 1.12 会输出 1066359849,这叫做未定义行为,你可以在 这里 了解更多。

尝试使用整数格式说明符输出字符串会给出其第一个字符的 ASCII 码,在这个例子中,即字符 H 的码 72,单个字符的输出也是如此。最后,输出布尔值会给出 1(如果为真)或 0(如果为假)。

字符串本身就是数组,因此在这里输出一个数组会给出该数组第一个位置的值,输出方式取决于它的类型(整数、浮点数、字符、布尔值)。

  • 字符串格式说明符 - %s

这个格式说明符,代表字符串,负责输出字符串(显而易见):

printf("Hello, %s!", "World");
Hello, world!

我们也可以用它来输出非字符串值:

printf("%s", 103);
printf("%s", true);
printf("%s", 'H');
printf("%s", 1.12);
g

H
)

数字 103 被当作 ASCII 码处理,因此输出了字符 g,同样,下面那个奇怪的符号,布尔值为真,即 1 被打印出来,更简单地说,字符 'H' 被原样打印出来。但浮点数 1.12 怎么了?记得 未定义行为 吗?是的,1.12 结果是一个巨大的整数,这个整数不断溢出(它的值除以 255)多次,直到得到一个介于 0254 之间的数字,在这个例子中是 40,这是字符 )ASCII 码。

同样,像整数格式说明符一样,这个说明符也接受预定义常量、变量和函数:

#define NAME "Max"
new message[] = “Hello there!;

printf("His name is %s", NAME);
printf("Hey, %s", message);
printf("%s work", #Great);
His name is Max
Hey, Hello there!
Great work
  • 浮点数格式说明符 - %f

这个说明符 - 代表浮点数 - 正如其名字所示,它输出浮点数。在前面的部分,我们尝试使用整数格式说明符来输出浮点数,然后我们遇到了未定义行为,但现在,我们了解了这个说明符,我们可以安全地输出浮点数而没有问题。

printf("%f", 1.235);
printf("%f", 5);
printf("%f", 'h');
1.235000
0.000000
0.000000

浮点数 1.235 输出得很好,尽管有一些填充,不过,其他所有输出都是 0.000000,基本上是 0,这是因为 %f 说明符只会输出浮点数,换句话说,就是在小数点前后没有固定数字的数字;即小数点可以浮动。

为了解决这个问题,我们只需添加小数部分:

printf("%f", 5.0);
printf("%f", 'h' + 0.0);
5.000000
104.000000

尽管 %f 是最常用的浮点数占位符,%h 说明符也基本上做同样的事情:

printf("%h", 5.0);
5.000000
  • 字符格式说明符 - %c

这个说明符,代表字符,类似于字符串占位符,但它只输出一个单独的字符,让我们观察以下示例:

printf("%c", 'A');
printf("%c", "A");
printf("%c", "Hello");
printf("%c", 105);
printf("%c", 1.2);
printf("%c", true);
A
A
H
i
s

如你所见,传递一个字符串只会输出第一个字符,而传递一个数字会输出与该数字的 ASCII 码匹配的字符 (布尔值分别转换为 0 和 1)。

  • 十六进制格式说明符 - %x

以下说明符将我们传递的值输出为十六进制数,简单来说,就是将数字从某个基数转换为基数 16。

printf("%x", 6);
printf("%x", 10);
printf("%x", 255);
6
A
FF

正如我们在之前的部分看到的,传递非整数值时,会将它们转换为各自的整数值,并以十六进制数形式输出;

printf("%x", 1.5);
printf("%x", 'Z');
printf("%x", "Hello");
printf("%x", true);
3FC00000
5A
48
1

第一个值 1.5 会在转换为整数后产生未定义行为 (1069547520),然后将结果整数以十六进制形式输出 (3FC00000)。字符 'Z'ASCII 值(90)会被转换为十六进制(5A)。字符串 "Hello" 仅会将第一个字符(H)的 ASCII 值(72)转换为十六进制(48)。布尔值 true 会以十六进制输出(1),即转换为(1),(false 将输出 0)。.

  • 二进制格式说明符 - %b

以下说明符,代表“二进制”,用于将传递的值以二进制数形式打印,传递字符时会将其 ASCII 码转换为二进制,字符串的处理方式相同,仅考虑第一个字符,布尔值分别视为 true 和 false,浮点数属于未定义行为的情况,整数和十六进制数则会转换为二进制并输出。

printf("%b", 0b0011);
printf("%b", 2);
printf("%b", 2.0);
printf("%b", 0xE2);
printf("%b", 'T');
printf("%b", "Hello");
printf("%b", true);
11
10
1000000000000000000000000000000
11100010
1010100
1001000
1
  • 字面量 %

类似于默认的转义字符(\),编译器将(%)视为特殊字符,因此将其序列视为占位符,只要(%)后面跟着一个字符,它就会被视为格式说明符,即使它无效,我们来观察这两种情况;

printf("%");
printf("你好 %");
printf("% 世界");
printf("你好 % 世界");
%
你好 %
世界
你好 世界

如你所见,单独的(%)作为一个独立的序列会被输出,但如果它后面跟着空格或其他字符,则会输出一个空格。为了解决这个问题,我们使用另一个百分号进行转义,如下所示;

printf("这是一个百分号 %%, 我们只是需要对它进行转义!");
这是一个百分号 %, 我们只是需要对它进行转义!

当然,这仅涉及支持格式化的函数,例如 printfformat,例如,尝试使用 print 函数输出百分号时不需要转义。

  • %q 说明符

这个说明符在我们的主要话题中并不重要,它主要用于在处理 SQLite 时转义敏感字符串,相信我,没有人愿意陷入 Bobby tables 的情况。

回到我们介绍占位符时,我们提到了一些与之相关的特定公式,作为提醒,以下是它们;

%[flags][width][.precision]type

到目前为止,我们只谈到了(%)符号和类型字段,其他的是可选的,但每一个在不同情况下都是有效的,你可以包括它们以更好地控制你的值在输出时的处理方式。

  • 宽度字段

这个字段负责指定最小字符输出,如果需要,可以省略,只需将其值指定为一个整数,我们来看一些例子;

printf("%3d", 5555);
printf("%3d", 555);
printf("%3d", 55);
printf("%3d", 5);
5555
555
55
5

我们指示说明符将输出锁定为 3 个字符或更多,最初,输出 4 和 3 个字符长的数字没有问题,但短于 3 个字符的字符被用空格填充,以使输出宽度一致。还有动态宽度值的能力,为此,我们使用星号(*)。

printf("%*d", 5, 55);
     55

首先,我们传递宽度的值 5,然后是我们想要输出的值 55,因此占位符输出至少 5 个字符,即 5 减去 2,给我们 3 个填充的空格。

  • 标志字段

这个字段与宽度字段配合得很好,因为宽度字段指定了最小字符输出,这个字段则填充留下的空白部分,用你告诉它的任何东西。如果留下了空格,则不会填充。

printf("%3d", 55);
printf("%5x", 15);
printf("%2f", 1.5)
055
0000F
01.500000

第一个数字 55 由于十进制参数的宽度不足一个字符,因此用一个 0 进行了填充。对于 15,它被转换为相应的十六进制值 F,并用 4 个 0 填充,以满足占位符的宽度要求。注意,只有小数点前的数字被填充。动态宽度值也适用,只需包含星号(*),传递一个值,即可看到效果;

printf("%0*d", 5, 55);
00055
  • 精度字段

精度字段通常指定输出的最大限制,取决于特定的格式类型。对于浮点数类型,它指定输出应该四舍五入的小数点右侧的位数。对于字符串类型,它限制应输出的字符数,超出部分会被截断。

printf("%.2f", 1.5);
printf("%.*f", 10, 1.5);
printf("%.5s", "Hello world!");
printf("%.*s", 7, "Hello world!");
1.50
1.5000000000
Hello
Hello w

正如你所见,动态精度值可以与浮点和字符串占位符一起使用。

利用精度字段的一个非常酷的技巧是获取子字符串,现在,有很多方法可以做到这一点,而不考虑原生的 strfind 函数,别忘了我们在 Slicestrlib 包中获得的那些惊人函数。

让我们看看如何仅使用精度字段获得相同的结果。

substring(const source[], start = 0, length = -1)
{
new output[256];

format(output, sizeof(output), "%.*s", length, source[start]);
return output;
}

让我们尝试解读这段代码,我们简单地传递源字符串(我们要提取的字符串)、起始位置(我们开始提取的槽位)和我们想提取的字符长度。

我们的返回值将根据以下占位符格式化 %.*s,我们包含了精度字段,并使用它来确定一个动态值,这个值将是提取字符的长度,然后通过添加 source[start] 提供提取的起始点,这样我们就从函数参数中传递的 start 槽位开始提取。

让我们调用这个函数看看结果如何:

new message1[] = "Hello!", message2[] = "I want an apple!";

print(substring(.source = message1, .start = 1, .length = 3));
print(substring(.source = message2, .start = 7, .length = 8));
ell
an apple

简单吧?附带的 trivia 小知识,传递一个 负值 作为提取长度会导致输出从 start 槽位开始的所有字符。另一方面,传递 0 作为提取长度会返回一个空值。

让我们看一下这些情况:

new message3[] = "Arrays start at 1, says the Lua developer!";

print(substring(message3)); // 默认起始位置 = 0,长度 = -1
print(substring(message3, .length = 6)); // 默认起始位置 = 0,长度 = 6
print(substring(message3, 7, 10)); // 起始位置 = 7,长度 = 10
print(substring(message3, strlen(message3) - 14)); // 起始位置 = 28,默认长度 = -1
print(substring(message3, strlen(message3) - 14, 3)); // 起始位置 = 28,长度 = 3
Arrays start at 1, says the Lua developer!
Arrays
start at 1
Lua developer!
Lua

使用示例

将我们迄今为止所看到的付诸实践,我们可以以几乎任何方式格式化字符串,到目前为止,我们主要在控制台工作,使用 printprintf 函数输出数据,实际上主要是 printf,因为它本身支持动态格式化字符串,因此函数名中有个 f。

但在现实世界中,大多数人不喜欢看终端,它们对普通用户来说太可怕、复杂,正如你们所知道的,客户端消息 会显示在游戏屏幕上,而不是控制台,然而,这些不能动态格式化,更像是 print 函数的行为,为了绕过这个限制,我们使用另一个非常有效的函数,称为 format,我们不会深入其定义,因为我们已经在早期部分解释过了,(参考 this),但这里是其语法的提醒:

format(output[], len, const format[], \{Float, _}: ...}

让我们看这些示例;

示例 1: 玩家名称 - 假设服务器上有一个 ID 为 9 的玩家,叫做 Player1

// MAX_PLAYER_NAME 是一个预定义常量,其值为 24,我们添加了 1 以考虑空结束符,感谢 Pottus 指出这一点
new playerName[MAX_PLAYER_NAME + 1], output[128], playerid = 9;

GetPlayerName(playerid, playerName, MAX_PLAYER_NAME);
format(output, sizeof(output), "[信息]: ID 为 %d 的玩家名为 {EE11CC}%s.", playerid, playerName);SendClientMessageToAll(0, output);

[信息]: ID 为 9 的玩家名为 Player1

很简单,我们只需获取玩家名称并开始格式化字符串,%d 占位符负责显示 playerid 变量,其值为 9%s 占位符显示 playerName 字符串,这个字符串包含了玩家的名字,这是通过 GetPlayerName 函数获得的。

然后我们使用 SendClientMessageToAll 函数将格式化后的字符串显示给服务器上的所有人,注意其第一个参数的 0 值表示黑色,即消息的颜色,嵌入的十六进制值 {FFFF00} 导致玩家名称呈现黄色。

示例 2: 游戏内时钟 - 显示游戏中的当前时间

new output[128], hours, minutes, seconds;

gettime(hours, minutes, seconds);
format(output, sizeof(output), "现在是 %02d:%02d %s", hours > 12 ? hours - 12 : hours, minutes, hours < 12 ? ("AM") : ("PM"));
SendClientMessageToAll(0, output);

我们再次利用 gettime 函数将小时、分钟和秒分别存储在变量中,然后将它们组合成一个格式良好的字符串,我们利用宽度字段 %02d 将 0 到 9 之间的值填充为另一个 0,以避免输出像(“现在是 5:9 PM”)这样的结果,如你所见。

现在是 06 :17 PM

示例 3: 死亡消息 - 当玩家死亡时输出消息,玩家名字按其各自颜色显示

public OnPlayerDeath(playerid, killerid, WEAPON:reason)
{
// MAX_PLAYER_NAME 是一个预定义常量,其值为 24,我们添加了 1 以考虑空结束符,感谢 Pottus 指出这一点
new message[144], playerName[MAX_PLAYER_NAME + 1], killerName[MAX_PLAYER_NAME + 1];

GetPlayerName(playerid, playerName, MAX_PLAYER_NAME);
GetPlayerName(killerid, killerName, MAX_PLAYER_NAME);

format(message, sizeof(message), "{%06x}%s {000000}杀害了 {%06x}%s", GetPlayerColor(killerid) >>> 8, killerName, GetPlayerColor(playerid) >>> 8, playerName);
SendClientMessageToAll(0, message);

return 1;
}

给定以下连接玩家列表:

ID玩家
0Compton
1Dark
5Player1
6Bartolomew
11unban_pls

假设 playerid0 的玩家杀死了 playerid6 的玩家,格式化消息应该显示为 “{FF0000}Compton {000000} 杀害了 > {0000FF}Bartolomew”,这将向服务器上的每个人发送以下客户端消息:

Compton ­ 杀害了 ­ Bartolomew

我为使用 按位逻辑移位 而可能让你感到困惑感到抱歉,它仅用于将 GetPlayerColor 函数返回的十进制数字转换为代表颜色的十六进制数字,移位本身用于省略 -alpha- 空间,更多信息我强烈建议你查看 Kyosaur这个教程

自定义说明符

使用我们迄今为止介绍的格式说明符已经足够,你可以用这些极其强大的工具做各种事情,但没有什么能阻止我们向我索取更多,这真是贪心啊。多亏了 Slice 受到 sscanf 的启发,他创建了一个令人惊叹的 include,formatex,添加了几个新的说明符,极大地简化了日常的 pawn 工作。但这还不是全部,你还可以创建自己的说明符以满足需求,虽然听起来很酷,但过程非常简单。

仅仅为了测试,让我们做一些简单的事,比如输入一个字符串,然后以链接的形式返回它 (https://www.string.com);

FormatSpecifier<'n'>(output[], const param[]) {
format(output, sizeof(output), "https://www.%s.com", param);
}

就这么简单,于是,强大的 %n 说明符(缩写为 Newton,因为它非常酷且复杂 😉)诞生了,让我们测试一下这个小家伙:

printf("%n", "samp");

https://www.samp.com

备注

不要让这个示例限制了你对自定义说明符可能实现的期望,主版本页面上有更好的示例,请去查看

外部链接

类似的教程

相关的 包含/插件/贡献者

References