…………
下面要讲的是字符串使用的高级技巧:偏移和迭代。
2-1 准确地说是字符串ID的偏移。
看一下下面这个例子:
("describe_relation_to_s63",
[(store_script_param_1, ":relation"),
(store_add, ":normalized_relation", ":relation", 100),
(val_add, ":normalized_relation", 5),
(store_div, ":str_offset", ":normalized_relation", 10),
(val_clamp, ":str_offset", 0, 20),
(store_add, ":str_id", "str_relation_mnus_100", ":str_offset"),
(str_store_string, s63, ":str_id"),
]),
这个script是用来把玩家与各领主的关系用字符串描述出来并存到s63里,巧妙地利用了字符串ID的偏移量。这里的局部变量":str_offset"就是偏移量。前面4行就是在算这个偏移量。(store_add, ":str_id", "str_relation_mnus_100", ":str_offset"), 就是把基础字符串的ID加上字符串ID的偏移量存到":str_id"里。局部变量":str_id"就是目标字符串的ID了。
比如玩家与某领主的关系是47,也就是这里的":relation"为47。 那么":str_offset"算出来就是15,而":str_id"就是"str_relation_mnus_100"加15,得到的就是"str_relation_plus_50",也就是s63会输出"Friendly"。这和关系的数值是完全符合的。
下面是要用到的所有字符串:
("relation_mnus_100", "Vengeful"), # -100..-94
("relation_mnus_90", "Vengeful"), # -95..-84
("relation_mnus_80", "Vengeful"),
("relation_mnus_70", "Hateful"),
("relation_mnus_60", "Hateful"),
("relation_mnus_50", " Hostile"),
("relation_mnus_40", " Angry"),
("relation_mnus_30", " Resentful"),
("relation_mnus_20", " Grumbling"),
("relation_mnus_10", " Suspicious"),
("relation_plus_0", " Indifferent"),# -5...4
("relation_plus_10", " Cooperative"), # 5..14
("relation_plus_20", " Welcoming"),
("relation_plus_30", " Favorable"),
("relation_plus_40", " Supportive"),
("relation_plus_50", " Friendly"),
("relation_plus_60", " Gracious"),
("relation_plus_70", " Fond"),
("relation_plus_80", " Loyal"),
("relation_plus_90", " Devoted"),
如果不用字符串ID的偏移,那就很痛苦了,要不停地else_try且is_between地进行20遍。
需要说明的是,要使用字符串ID的偏移,必须使用str开头的输出字符串。因为只有str开头的输出字符串才有明确的数字ID和有规律的顺序定义,可以用来相加,实现字符串ID的偏移,而@开头的输出字符串没有明确的数字ID,定义也太随意,不能实现字符串ID的偏移。所以使用str开头的输出字符串还是使用@开头的输出字符串不仅仅是一个单纯的习惯问题,而是要考虑实际的要求。
再举一个稍微简单一些的例子:
(
"custom_battle_optional",0,
"From here the time of day and the weather can be changed.",
"none",
[
(store_add, ":rain_settings", "str_custom_battle_rain_0", "$g_rain_settings"),
(str_store_string, s1, ":rain_settings"),
],
[
("rain",[],"Rain/Snow ({s1})",
[
(val_add, "$g_rain_settings", 1),
(val_mod, "$g_rain_settings", 7),
]),
("next",[],"Next",
[
(jump_to_menu, "mnu_custom_battle_config_finish"),
]),
]
),
("custom_battle_rain_0", "None"),
("custom_battle_rain_1", "Rain - Light"),
("custom_battle_rain_2", "Rain - Medium"),
("custom_battle_rain_3", "Rain - Heavy"),
("custom_battle_rain_4", "Snow - Light"),
("custom_battle_rain_5", "Snow - Medium"),
("custom_battle_rain_6", "Snow - Heavy"),
这个是自定义战斗里关于天气设定的源代码,当然这是我改写后的,原本的源代码在下面:
(
"custom_battle_optional",0,
"From here the time of day and the weather can be changed.",
"none",
[
(try_begin),
(eq, "$g_rain_settings", 0),
(str_store_string, s1, "str_custom_battle_rain_0"),
(else_try),
(eq, "$g_rain_settings", 1),
(str_store_string, s1, "str_custom_battle_rain_1"),
(else_try),
(eq, "$g_rain_settings", 2),
(str_store_string, s1, "str_custom_battle_rain_2"),
(else_try),
(eq, "$g_rain_settings", 3),
(str_store_string, s1, "str_custom_battle_rain_3"),
(else_try),
(eq, "$g_rain_settings", 4),
(str_store_string, s1, "str_custom_battle_rain_4"),
(else_try),
(eq, "$g_rain_settings", 5),
(str_store_string, s1, "str_custom_battle_rain_5"),
(else_try),
(eq, "$g_rain_settings", 6),
(str_store_string, s1, "str_custom_battle_rain_6"),
(try_end),
],
[
("rain",[],"Rain/Snow ({s1})",
[
(try_begin),
(eq, "$g_rain_settings", 0),
(assign, "$g_rain_settings", 1),
(else_try),
(eq, "$g_rain_settings", 1),
(assign, "$g_rain_settings", 2),
(else_try),
(eq, "$g_rain_settings", 2),
(assign, "$g_rain_settings", 3),
(else_try),
(eq, "$g_rain_settings", 3),
(assign, "$g_rain_settings", 4),
(else_try),
(eq, "$g_rain_settings", 4),
(assign, "$g_rain_settings", 5),
(else_try),
(eq, "$g_rain_settings", 5),
(assign, "$g_rain_settings", 6),
(else_try),
(eq, "$g_rain_settings", 6),
(assign, "$g_rain_settings", 0),
(try_end),
]),
("next",[],"Next",
[
(jump_to_menu, "mnu_custom_battle_config_finish"),
]),
]
),
("custom_battle_rain_0", "None"),
("custom_battle_rain_1", "Rain - Light"),
("custom_battle_rain_2", "Rain - Medium"),
("custom_battle_rain_3", "Rain - Heavy"),
("custom_battle_rain_4", "Snow - Light"),
("custom_battle_rain_5", "Snow - Medium"),
("custom_battle_rain_6", "Snow - Heavy"),
这段源代码在两个地方的处理得都不够艺术,都是用的臃肿且繁琐的try_begin - else_try - try_end结构。
第一个地方就是字符串的处理上,用符串ID的偏移的话,不管多少种下雨强度或下雪强度,2行就能搞定了。
第二个地方是变量的循环变化上,用val_add(自加)结合val_mod(取余)也是2行就能搞定了。
这段代码行数多但代码质量却很低,写得也很累。所以有时候单纯列举说写了几千几万行的代码一点意义都没有。实现同样的功能,写得精的话可能就十几行搞定,写得不精的话,可能代码行数就要多几倍,几十倍甚至几百倍。
我写的Pentominoes(伤脑筋的十二块),总共就600多行代码,其中还包括了45个关卡的数据。如果用Module System里常规的方法写,别的不论,光45个关卡的数据就得花7200行代码。如果反编译Pentominoes的txt代码的话,到头来只能证明这种反编译行为是一种“聪明的白痴行为”,反编译45个关卡的txt数据最终得到的结果将会是7200行代码,而不是我写的那50多行代码。一点题外话。
最后,字符串ID的偏移只是ID偏移的一种,任何有数字ID的各种ID都能这样偏移。连script都能偏移。下面就是script偏移的一个例子。
该片断来自于我写的Pentominoes的源代码。
(store_add, ":dest_script", "script_init_stages_data_1", "$g_stage_no"),
(call_script, ":dest_script"),
我写关卡数据都储存在一个一个独立的script里的。不同的关卡就得调用不同的script,跳关卡的时候,自然不可能用臃肿且繁琐的try_begin - else_try - try_end结构来根据关卡序号来调用相应的script,那样要判断45次。所以这里就用到了script-ID的偏移,用第1关的script的ID加上关卡序号(从0开始),得到":dest_script"(目标script),在call_script来调用":dest_script"(目标script)就行了。第1关的关卡序号是0,所以调用的就是"script_init_stages_data_1",第2关的关卡序号是1,自然调用的就是"script_init_stages_data_1"后面的一个script,就是"script_init_stages_data_2",依次类推。最终就用上面2行就搞定了。
任何有数字ID的各种ID都能这样偏移才是我真正要讲的内容。只是因为这里主题已经定了,所以就侧重以字符串ID的偏移为例子来讲解。
2-2 字符串的迭代:
看下面的例子,这是从script_update_troop_notes里截取的一段,是关于领主的封地的。
(assign, ":num_centers", 0),
(str_store_string, s58, "@nowhere"),
(try_for_range_backwards, ":cur_center", centers_begin, centers_end),
(party_slot_eq, ":cur_center", slot_town_lord, ":troop_no"),
(try_begin),
(eq, ":num_centers", 0),
(str_store_party_name_link, s58, ":cur_center"),
(else_try),
(eq, ":num_centers", 1),
(str_store_party_name_link, s57, ":cur_center"),
(str_store_string, s58, "@{s57} and {s58}"),
(else_try),
(str_store_party_name_link, s57, ":cur_center"),
(str_store_string, s58, "@{s57}, {s58}"),
(try_end),
(val_add, ":num_centers", 1),
(try_end),
经过迭代,s58就包含了该领主所有的封地了,仅仅就用了两个字符串寄存器s57和s58就解决了。如果用常规的方法,一个封地一个封地地判断,并且每个封地都占用一个字符串寄存器,一来繁琐,二来字符串寄存器数量是有限的(65个),领主或者玩家的封地一多,字符串寄存器就完全不够用了。
迭代原理比较难理解,也许你很难明白为什么频繁出现的s57和s58里寄存的字符串能不相互覆盖或者冲突,为什么能保证输出的各个字符串是相互区别开的。
那就这样理解吧,s57和s58是两个存字符串的“容器”。s58相当于瓶子,s57相当于杯子,而":cur_center"相当于水。这里首先是把"@nowhere"放到s58里,如果领主没有封地,那么s58就只有"@nowhere"了。如果有1个封地":cur_center",那么就是直接把它放到s58,覆盖掉"@nowhere",继续判断,如果有第2个封地,那么就先把":cur_center"放到s57这个杯子里,然后“倒”到s58这个瓶子里,事实上是拿"@{s57} and {s58}"来覆盖s58,也就相当于把瓶子里的水和杯子里倒出来混合之后又倒回到瓶子里。如果有更多的封地也是按照这个原理,把新的s57和旧的s58连在一起覆盖掉旧的s58,形成新的s58。依次迭代,最终的s58里就含有所有的封地了。而且符合自然语言的语法规则,一个对象的时候就直接显示,两个对象就用and连接起来,多个对象的话,最后两个对象用and连接起来,其他的用逗号隔开。而s57里还含有最后一个封地的字符串,不过不要紧,s57再次使用的时候,s57里的内容会被新的内容覆盖,不会影响到新的内容。
还是模拟一下迭代过程比较好,比如某个领主有5个封地:c1,c2,c3,c4,c5。注意了,下面的“=”不是等号,是赋值号。而且赋值是至右向左的,要从右往左来看。而且这个“=”也是方便说明才使用的,下面也都是伪代码。真正的代码里是用的str_store_party_name_link和str_store_string来完成字符串的赋值的。
第1次满足条件的时候,s58 = c1。
第2次满足条件的时候,先s57 = c2,此时"@{s57} and {s58}"里的内容就是"@c2 and c1"。然后s58 = "@{s57} and {s58}"就是 s58 = c2 and c1。
第3次满足条件的时候,先s57 = c3,此时"@{s57}, {s58}"里的内容就是"@c3,c2 and c1"。然后s58 = "@{s57}, {s58}"就是 s58 = c3,c2 and c1。
第4次满足条件的时候,先s57 = c4,此时"@{s57}, {s58}"里的内容就是"@c4,c3,c2 and c1"。然后s58 = "@{s57}, {s58}"就是 s58 = c4,c3,c2 and c1。
第5次满足条件的时候,先s57 = c5,此时"@{s57}, {s58}"里的内容就是"@c5,c4,c3,c2 and c1"。然后s58 = "@{s57}, {s58}"就是 s58 = c5,c4,c3,c2 and c1。
这样模拟一下,应该能看得更明白了,更多的封地也一样地迭代出来的。不光封地,其他的对象也能循环迭代,这只是一个例子而已。
另外,根据迭代原理,先满足条件的会在后面,后满足条件的会在前面。所以这里用的try_for_range_backwards,倒过来从尾向头判断,以保证显示出来的是正着的。
对比字符串ID的偏移,可以发现,这里都是使用的@开头的输出字符串。硬要用str开头的输出字符串也可以,不过那样做就太呆板,不够灵活快捷了。这也验证了输出字符串类型的选择不单纯是一个习惯问题。
最后是一个综合的例子,偏移和迭代同时使用。
(str_store_string, s0, "@ "),
(try_for_range, ":cur_ek_slot", ek_item_0, num_equipment_kinds),
(troop_get_inventory_slot, ":cur_item", "trp_player", ":cur_ek_slot"),
(gt, ":cur_item", -1),
(troop_get_inventory_slot_modifier, ":item_mod", "trp_player", ":cur_ek_slot"),
(store_add, ":out_string", "str_imod_plain", ":item_mod"),
(str_store_string, s2, ":out_string"),
(str_store_item_name, s1, ":cur_item"),
(str_store_string, s0, "@{s0}^{s2}{s1}"),
(try_end),
这样迭代之后,s0里面就包含了玩家身上10件装备以及他们的前缀,包括4件武器,4件盔甲,1匹马和1份食物(如果开启了食物栏)。如果对应的格子是空的,那么就跳过不显示,只罗列非空的格子里的物品以及他们的前缀。前缀的处理用的是字符串ID的偏移,而同时显示几件物品及前缀用的是字符串的迭代。
如果不用字符串ID的偏移的话,代码就很臃肿且低效了。因为过多地try会使得效率降低,而且每件物品,都要把这些前缀从头到尾try一遍,效率就更低了。而这里是通过相加直接找到目标字符串,是无判断过程的。而不用迭代的话,最终输出的字符串里就需要花10个reg寄存器来判断每个格子是否是空的,要花20个字符串寄存器来分别纪录物品名字以及物品的前缀。如果是要纪录玩家物品栏里的所有物品及各自前缀的话,是必然要使用偏移和迭代。
当然了,这只是我制造出来的一个例子,例子本身可能没有什么价值,价值都在例子折射出来的东西上。
下面是要用到的字符串:
("imod_plain", " "),
("imod_cracked", "Cracked "),
("imod_rusty", "Rusty "),
("imod_bent", "Bent "),
("imod_chipped", "Chipped "),
("imod_battered", "Battered "),
("imod_poor", "Poor "),
("imod_crude", "Crude "),
("imod_old", "Old "),
("imod_cheap", "Cheap "),
("imod_fine", "Fine "),
("imod_well_made", "Well Made "),
("imod_sharp", "Sharp "),
("imod_balanced", "Balanced "),
("imod_tempered", "Tempered "),
("imod_deadly", "Deadly "),
("imod_exquisite", "Exquisite "),
("imod_masterwork", "Masterwork "),
("imod_heavy", "Heavy "),
("imod_strong", "Strong "),
("imod_powerful", "Powerful "),
("imod_tattered", "Tattered "),
("imod_ragged", "Ragged "),
("imod_rough", "Rough "),
("imod_sturdy", "Sturdy "),
("imod_thick", "Thick "),
("imod_hardened", "Hardened "),
("imod_reinforced", "Reinforced "),
("imod_superb", "Superb "),
("imod_lordly", "Lordly "),
("imod_lame", "Lame "),
("imod_swaybacked", "Swaybacked "),
("imod_stubborn", "Stubborn "),
("imod_timid", "Timid "),
("imod_meek", "Meek "),
("imod_spirited", "Spirited "),
("imod_champion", "Champion "),
("imod_fresh", "Fresh "),
("imod_day_old", "Day Old "),
("imod_two_day_old", "Two Day Old "),
("imod_smelling", "Smelling "),
("imod_rotten", "Rotten "),
("imod_large_bag", "Large Bag "),
这一节会讲一下字符串里的变量问题。
下面例子里的字符串都是字符常量。
(display_message,"@The door is locked.",0xFFFFAAAA),
如果要在字符串里安插变量,就得用寄存器了,而不能直接把数值变量或者字符串变量直接插在字符串里。数值变量必须赋值到一个reg类型的寄存器里,比如reg0,reg1等等。而字符串变量,比如兵种的名字,物品名字,必须赋值到字符串寄存器里,也就是s0,s1这样的寄存器。赋值的时候,不需要也不能加{}号。 而安插到字符串里就必须加{}号,如{reg0},{s0}等。
下面是从Custom Commander截取的一小段代码。
(try_begin),
(eq, ":kinds_of_upgrade_troop", 0),
(str_store_troop_name_by_count, s1, ":upgrade_troop", ":upgrade_size"),
(assign, reg1, ":upgrade_size"),
(str_store_string, s0, "@{reg1} {s1} can upgrade."),
(else_try),
(eq, ":kinds_of_upgrade_troop", 1),
(str_store_troop_name_by_count, s1, ":upgrade_troop", ":upgrade_size"),
(assign, reg1, ":upgrade_size"),
(str_store_string, s0, "@{reg1} {s1} and {s0}"),
(else_try),
(str_store_troop_name_by_count, s1, ":upgrade_troop", ":upgrade_size"),
(assign, reg1, ":upgrade_size"),
(str_store_string, s0, "@{reg1} {s1}, {s0}"),
(try_end),
这两句就是赋值过程,一个是字符串的,一个是数值的。
(str_store_troop_name_by_count, s1, ":upgrade_troop", ":upgrade_size"),
(assign, reg1, ":upgrade_size"),
字符串的这个操作叫赋值可能不大合适,但意思是差不多的。
这几句就是具体的使用。
(str_store_string, s0, "@{reg1} {s1} can upgrade."),
(str_store_string, s0, "@{reg1} {s1} and {s0}"),
(str_store_string, s0, "@{reg1} {s1}, {s0}"),
你就不能像下面这样使用:
(str_store_string, s0, "@":upgrade_size" ":upgrade_troop" can upgrade."),
(str_store_string, s0, "@":upgrade_size" ":upgrade_troop" and {s0}"),
(str_store_string, s0, "@":upgrade_size" ":upgrade_troop", {s0}"),
全局变量(以$开头的)也不能直接插到字符串里。
字符串里出现的变量只能是reg类型的寄存器和s类型的寄存器。 一个管数值,一个管字符串。同一个字符串里不要使用一样的寄存器,迭代除外。用迭代的前提是你要明白迭代原理并能熟练使用迭代,不然使用一样的寄存器,很容易就造成相互覆盖,显示出来的内容都是错误的。