零基础开发 era 游戏 #10 业务逻辑(交互反馈)

数值运算和逻辑运算#

运算符优先级#

优先级从高到低排列:

类型符号是否代入复合运算
否定运算符~ !
算术运算符* / %
+ -
位运算符<< >>
比较运算符< <= > >=
== !=
逻辑运算符& | ^
&& || !& !| ^^
三目运算符判定条件 ? 判定通过 # 判定未通过

否定运算符#

由于 Emuera 的数据类型只有 数值字符串,没有布尔值bool
因此用 数值 类型的 非0 / 0 表示其他语言中的 true / false

! 的本质是「not」(否定):

  • not 10
  • not 01

~ 的本质是「按位取反」(~n-n-1,至于为什么,详见《二进制表达整型》):

  • ~ -10
  • ~ 0-1
@SYSTEM_TITLE
    CALL TEST_NOT
    QUIT


@TEST_NOT
    #DIM tmp, 2
    WHILE 1
        CLEARLINE LINECOUNT
        tmp:0 = ~tmp:0
        tmp:1 = !tmp:1
        PRINTFORMLC ~tmp:0={tmp:0}
        PRINTFORML 结果=%tmp:0 ? "true" # "false"%
        PRINTFORMLC !tmp:1={tmp:1}
        PRINTFORML 结果=%tmp:1 ? "true" # "false"%
        INPUTMOUSEKEY
    WEND

算术运算符#

+、减 -、乘 *、除 /、取余 %,有手就行,不多说了。

小数乘法

这里稍微提一下神奇的 TIMES 命令,一个非常实用的 隐藏功能
官方文档 中的确没有提到这个命令,但它确确实实是存在的。

由于本家 / 私家改造版均未使用现代的版本控制系统(指 Git)对源码进行项目管理。
因此这里以 EmueraEE+EM 的源码为例。

Emuera/GameProc/Function/BuiltInFunctionCode.cs 注册系统内置函数的枚举值:

59
60
61
62
    DRAWLINE, // 画面の左端から右端まで----と線を引く。
    BAR,      // [*****....]のようなグラフを書く。BAR (変数) , (最大値), (長さ)
    BARL,     // 改行付き。
    TIMES,    // 小数計算。TIMES (変数) , (小数値)という形で使う。

Emuera/GameProc/Function/Instraction.Child.cs 构造实现具体功能的密封类:

1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
private sealed class TIMES_Instruction : AbstractInstruction
{
    public TIMES_Instruction()
    {
        ArgBuilder = ArgumentParser.GetArgumentBuilder(FunctionArgType.SP_TIMES);
        flag = METHOD_SAFE;
    }

    public override void DoInstruction(ExpressionMediator exm, InstructionLine func, ProcessState state)
    {
        SpTimesArgument timesArg = (SpTimesArgument)func.Argument;
        VariableTerm var = timesArg.VariableDest;
        if (Config.TimesNotRigorousCalculation)
        {
            double d = (double)var.GetIntValue(exm) * timesArg.DoubleValue;
            unchecked
            {
                var.SetValue((Int64)d, exm);
            }
        }
        else
        {
            decimal d = var.GetIntValue(exm) * (decimal)timesArg.DoubleValue;
            unchecked
            {
                //decimal型は強制的にOverFlowExceptionを投げるので対策が必要
                //OverFlowの場合は昔の挙動に近づけてみる
                if (d <= Int64.MaxValue && d >= Int64.MinValue)
                    var.SetValue((Int64)d, exm);
                else
                    var.SetValue((Int64)((double)d), exm);
            }
        }
    }
}

可以看到无论是否启用 严格计算 配置项,第二个参数都是先取为 double 类型。
但前文我们提过「在 era 里,所有数值变量都是整型(int64)」,那么要如何做到呢?

很简单,直接写数值本身就行了:

@SYSTEM_TITLE
    #DIM num
    WHILE 1
        PRINTL 请输入数值:
        INPUT
        SIF RESULT == 0
            CONTINUE
        num = RESULT
        PRINTFORML 输入的数值 num = {num} 即将提升 25\%(之后自动取整)
        ;num = num * 5 / 4  ; 等效写法
        TIMES num, 1.25
        PRINTFORML 提升 25\% 并向下取整后 num = {num}
        DRAWLINE
    WEND
    QUIT

位运算符#

左移 << 表示将 数值对象 左移 指定位数,高位丢弃,低位补 0。
在一定范围内,每向左移动一位,相当于 * 2

右移 >> 表示将 数值对象 右移 指定位数,低位丢弃,高位 补 0逻辑右移 / 补原符号位算数右移
(C# 是逻辑右移,Emuera 应该也是。)
在一定范围内,每向右移动一位,相当于 / 2

; 将 2 左移 9 位 = 2 乘以 9 次 2 = 2 的 10 次方 = 1024(十进制)
PRINTVL 2 << 9

逻辑运算符#

  • & 按位「and
  • | 按位「or
  • ^ 按位「异或xor
  • ^^ 不按位的「异或」
  • !& 不按位的「与非nand
  • !| 不按位的「或非nor

有需要请自行查阅相关资料,此处不再赘述。

实在要用、又搞不太明白的话,可以直接吃现成(使用系统内置函数):

  • GETBIT(num, bit) 获取指定 数值变量 的指定 的值
  • SETBIT num, bit 将指定 数值变量 的指定 设为 1
    等价于 num |= 1 << bit
  • CLEARBIT num, bit 将指定 数值变量 的指定 设为 0
    等价于 num &= ~(1 << bit)
  • INVERTBIT num, bit 将指定 数值变量 的指定 取反
    等价于 num ^= 1 << bit

逻辑短路

  • && 不按位的「与」
  • || 不按位的「非」

强烈推荐的用法,这个叫做「短路逻辑」运算符。

比如判断 条件1 && 条件2 通过,需要同时满足 条件 1条件 2(与)。
此时若未满足 条件 1,就不会再继续判断 条件 2 了(因为已经不可能通过判定了)。

同理,判断 条件1 || 条件2 通过,需要满足 条件 1条件 2 任一即可(或)。
此时若已满足 条件 1,就不会再继续判断 条件 2 了(因为已经确定通过判定了)。

在逻辑判断中一律使用「短路逻辑」运算符,可以省下大量不必要的运算。

比较运算符#

大于 >大于等于不小于 >=、大于 <小于等于不大于 <=、相等 ==、不相等 !=,有脑子就行,不多说了。

三目运算符#

三目运算符(也叫三元运算符),即「若……,则……;否则……」。

WHILE 1
    PRINTFORML 你丢了一个硬币,是% RAND:2 == 1 ? "正面" # "反面" %。
    INPUTMOUSEKEY
WEND

我当然知道「写 RAND:2 == 1」其实等价于「写成 RAND:2」。
因为 RAND:2 表示在 [0, 2-1] 这个范围(闭区间)内随机生成一个数值。
(当然了,还得是整数,Emuera 就没有小数。)
所以 RAND:2 的结果要么是 0,要么是 1,可以直接用来作为判断条件。
0 = false / 非0 = true
简写会更优雅,写全会更符合直觉以便于理解;具体如何选择见仁见智,随你高兴。

字符串可以直接使用 \@ 三元运算表达式 \@ 的格式。

WHILE 1
    PRINTSL @"你丢了一个硬币,是% RAND:2 ? "正面" # "反面" %。"
    PRINTSL @"你又丢了一个硬币,是\@ RAND:2 ? 正面 # 反面 \@。"
    DRAWLINE
    INPUTMOUSEKEY
WEND

FORM 解析扩展用法#

@SYSTEM_TITLE
    CALL TEST_FORM
    QUIT


@TEST_FORM
    #DIM num = 123456
    #DIMS text = "田中脊髓剑"

    PRINTFORML [{num}]
    PRINTFORML [{num, 10}]
    PRINTFORML [{num, 10, LEFT}]
    ; 若指定的的长度小于实际长度 会被「撑大」到实际长度
    PRINTFORML [{num, 2}]

    PRINTFORML [%text%]
    PRINTFORML [%text, 20%]
    PRINTFORML [%text, 20, LEFT%]
    PRINTFORML [%text, 3%]

    ; 实际用途举例
    PRINTSL @"% "", 4 %这是最快的首行缩进写法。"
    PRINTL 真的,不骗你。

其他 Tips#

  • 数值变量 ++(递增)和 --(递减)运算符都是支持的。
  • 但我个人不推荐使用,哪怕你用 += / -= 都比这(可读性)强。
  • 所以也有 *=/=%=&=|=^=……等等写法。
    表示「自己和后面的值进行相应操作,再将自己保存为新的运算结果」。
  • 关于字符串赋值 ='= 的区别 之前 讲过了。
  • 字符串是可以和数值做「乘法 *」运算的,此时表示「重复该字符串多少次」。
  • 哦对了,字符串的 + / += 代表「字符串拼接」操作,以防万一还是提一下

流程控制#

条件判断#

SIF

注意 SIF 的代码块是不需要关闭的,它只会影响紧挨着它的下一行。

WHILE 1
    SIF RAND:2
        PRINTL * 这行文本只会随机出现(受 SIF 影响)。
        PRINTL + 这行文本一定会出现,因为 SIF 只能控制紧挨着它的下一行。
        PRINTL + 缩进也不行,跟你说了 Emuera 无视缩进,缩进只是给你自己看的。
    PRINTL
    INPUTMOUSEKEY
WEND

IF

@SYSTEM_TITLE
    WHILE 1
        CALL TEST_IF_ELSE
        INPUTMOUSEKEY
    WEND
    QUIT


@TEST_IF_ELSE
    #DIM rng

    IF 1
        PRINTL 测试 IF 通过
    ENDIF

    rng = RAND:2
    IF rng
        PRINTFORML 测试 ELSE 随机到一 rng={rng}
    ELSE
        PRINTFORML 测试 ELSE 随机到零 rng={rng}
    ENDIF

    rng = RAND:3
    IF rng == 1
        PRINTFORML 测试 ELSEIF 随机到一 rng={rng}
    ELSEIF rng == 2
        PRINTFORML 测试 ELSEIF 随机到二 rng={rng}
    ELSE
        PRINTFORML 测试 ELSEIF 随机到零 rng={rng}
    ENDIF

SELECT

形如 IF ELSEIF ELSEIF ELSE ENDIF 的多条件判断可以写成可读性更强的 SELECT

@SYSTEM_TITLE
    WHILE 1
        CALL TEST_SELECTCASE
        INPUTMOUSEKEY
    WEND
    QUIT


@TEST_SELECTCASE
    #DIM rng
    rng = RAND(1, 21)  ; 1 ~ 20
    SELECTCASE rng
        CASE 20
            SETCOLORBYNAME Red
            PRINTFORML 特等奖({rng})
        CASE IS <= 2  ; 1, 2
            SETCOLORBYNAME Yellow
            PRINTFORML 一等奖({rng})
        CASE 3, 4, 5
            SETCOLORBYNAME Blue
            PRINTFORML 二等奖({rng})
        CASE 6 TO 10  ; 6, 7, 8, 9, 10
            SETCOLORBYNAME Green
            PRINTFORML 三等奖({rng})
        CASEELSE
            SETCOLORBYNAME Gray
            PRINTFORML 参与奖({rng})
    ENDSELECT

循环#

所有循环都可以使用:

  • CONTINUE 跳过这一次循环,直接进入下一次循环
  • BREAK 终止当前循环

需要注意的是,BREAK 退出循环时,会继续将自变量 + 1(或其他步进值)再退出。
这是和其他大部分语言不一致的地方(大部分语言 break 会立刻退出当前循环)。

REPEAT

@SYSTEM_TITLE
    CALL TEST_REPEAT
    QUIT


@TEST_REPEAT
    #DIM cnt
    #DIM repeat_times = 10
    REPEAT repeat_times
        cnt += 1
        PRINTFORML 总之重复 {repeat_times} 次就行了 ({cnt}/{repeat_times})
        PRINTFORML 注意 REPEAT 会自动将 COUNT 作为自变量:cnt={cnt} COUNT={COUNT}
        PRINTL
    REND

可以近似地将 REPEAT, 循环次数 视为 FOR COUNT, 0, 循环次数 的简写。

若使用 VariableSize.csv 禁用 COUNT 后(需要 Emuera.EE v18 以及之后的版本)不能再使用 REPEAT 命令。
(会报错 COUNTが使用禁止変数になっているため、REPEATは使用できません

FOR

@SYSTEM_TITLE
    CALL TEST_FOR
    QUIT


@TEST_FOR
    #DIM cnt
    #DIM start_num = 100
    #DIM end_num = 110
    FOR cnt, start_num, end_num, 2
        PRINTFORML [{cnt}] 从 {start_num} 递增(每次 + 2)到 {end_num} 为止(不包括 {end_num})
    NEXT
    FOR cnt, 10, 0, -1
        PRINTFORML [{cnt}] 从 10 递减(每次 - 1)到 0 为止(不包括 0)
    NEXT

若省略第四个参数,则默认为「递增(每次 + 1)」。

WHILE

先判断,如果没有达到结束条件,才执行。

@SYSTEM_TITLE
    CALL TEST_WHILE
    QUIT


@TEST_WHILE
    #DIM cnt = 10
    WHILE cnt--
        PRINTFORML 当前的 cnt={cnt}
    WEND

LOOP

类似与其他语言里的 do-while 循环。
先执行,再判断是否达到结束条件,因此至少会循环一次。
比起普通的 while 循环来说,也会多循环一次(已经达到结束条件那一次)。

比如本文的例子,同样是从 10 递减到 0:

  • WHILE 是循环 10 次(9 ~ 0)
  • LOOP 是循环 11 次(10 ~ 0)
@SYSTEM_TITLE
    CALL TEST_LOOP
    QUIT


@TEST_LOOP
    #DIM cnt = 10
    DO
        PRINTFORML 当前的 cnt={cnt}
    LOOP cnt--

跳转(GOTO)#

注意:在任何情况下,你都不应该使用 GOTO
GOTO 可能会唐突地跳转到任何地方,没有任何逻辑可循。

虽然我们不用,但应该知道怎么用,才能看懂别人写的。

$label_name
PRINTL 请输入 quit 退出:
INPUTS
SIF RESULTS != "quit"
    GOTO label_name
PRINTL 成功退出

lackbfun © 2021 - 2024