可编程游戏的设想与起步

【注:此游戏最早的编译设想,其中部分逻辑已废弃,语法已修改,勿以此为准】

基于Unity的游戏内置编译器

两天时间写了十多个脚本,已经完成了一个基础的脚本格式的构建。

不知道起什么名字就好,但有区分的必要,暂且就叫内置语法。

脚本编写流程:

在游戏中选中对象,弹出脚本UI,填写提交后,如果提交内容不同于初始内容,则开始执行。

1、 脚本文本解析:

脚本文本按照语法规则拆分为tokens,其中包含多个分类——语法符号、算法符号、字符。对字符进一步分类,分类为变量、方法名、参数

2、tokens依次交由个体解析器,模块解析器,主解析器解析执行。

个体解析器:一个挂载到对象的Behavior类,可有可无,如果有,则先由其对脚本进行解析。个体解析器由玩家用内置语法书写。
个体解析器的作用是给单独的对象实现了特殊的编译,比如不允许此对象被移动,这样即使游玩者输入脚本进行更改也无法实现移动。

模块解析器:当前只写了一个实现简易语法的模块解析器,如up 2 ,delete
模块解析器应当可以启用和禁用,作用于全局,且应当有非常良好的拓展性。除了我写的基础模块由C#编写,其他应由社区玩家用内置语法编写。
模块解析器会是这个半引擎游戏创造拓展的重点。一方面为了让语法足够简单抽象,模块解析器将为语法提供足够简易全面的封装函数,另一方面启用和禁用模块,能直观地展现到游戏体验上。

主解析器:只由C#编写,在同一游戏版本中无法改变。

语法规则由拆分输入文本的脚本决定,主解析器只进行标准语法规则的映射执行,因而不需要频繁维护更改。

Q: 如何实现内置语法解析内置语法?

A: 用内置语法将方法写入对象脚本,利用重写特性对方法进行处理,即达到解析器的效果。

Q: 玩家模块解析器如何引入合并?

A:游戏可以主动读取在外置文件夹的脚本等资源,因而只需要提交打包的文件夹,合并排查问题保持可兼容。

3、方法的执行:

基础语法如Move等标注了方法名,利用映射获取参数类型,参数匹配后进入底层的C#代码执行。

其他语法均在基础语法上嵌套。全局语法封装到模块解析器中,全局脚本执行这类语法。

当前脚本规则:

[以下内容写在了内容的语法表里,离完整的语法还差很多内容]

[一般直接书写注释不影响代码执行,但建议用中括号包裹注释]
[大小写敏感]
[关键词和方法不可被用作变量命名]

[运算符]
“>”, “<”, “=”, “>=”, “<=”, “==”, “+=”, “-=”, “+”, “-“, “*”, “/“
[关键字表]
“if”, “or”, “and”, “break”, “return”, “destroy”, “enable”, “disable”, “enabled”,
“Name”, “this”, “Object”, “Camera”, “Mouse”, “Time”, “TimeSpeed”,
“x”, “y”, “z”, “v”, “up”, “down”, “north”, “west”, “south”, “east”,
“x+”, “x-“, “y+”, “y-“, “z+”, “z-“, “Now”, “Once”, “Loop”,”rotate.x”,”rotate.y”,”rotate.z”, “scale.x”,”scale.y”,”scale.z”, “move”, “force”, “meet”, “wait”, “write”,”delete”

[执行方法名表]

“Move”, “Rotate”, “Scale”, “Force”, “Meet”, “?Meet”, “OnClickLeft”, “OnClickRight”,
“OnHoldLeft”, “OnDragLeft”, “OnHoldLeftOut”, “OnDragLeftOut”, “selected”,
“Find”, “generate”, “Wait”, “Write”, “Delete”

[位置]

x y z

[简单位移旋转缩放]

x += 1
rotate.x = 90
scale x = 10

[方向]

up down
north west south east
x+[x轴正方向] x- y+ y- z+ z-
[在当前游戏中north对应z+ east对应x+ up当然也就对应y+]

up 2 [方向加值,固定语义为位移]

[三种周期]

[周期添加于方法前决定方法执行的时机]、

Now:[加语句][Now可省略,即直接写语句,这样的语句会在编辑完脚本后直接执行,并会在执行后删除]

Once:[加语句] [立即执行一次,不删除,之后会在每次游戏运行时进行此初始化]

Loop: [加语句][游戏中循环执行语句]

[多行执行语句]

Loop: {
scale.x += 1
rotate.x -= 1
}

[执行语句的书写]
[标准执行语句:]

[方法关键字]+{ [参数1],[参数2],… }

[参数间以逗号分隔。每个参数都会限定类型,参数会依据类型自动填充到方法中,未被填充的参数会转为string依次填充到未填充的位置,第一个位置默认为当前操作对象,无需填充]

[当前执行语句方法]

[Move]

Move{[移动对象,可略],[表方向和距离的向量],[移动类型],[移动时间]}

[三种移动类型]

Move{up 2,Instant}[瞬间,直接改变值]
Move{up 2,Easing}[缓动,变化先慢后快再慢,符合运动学原理]
Move {up 2,Smooth}[平滑,均匀变化]

[Rotate]
[Scale]

[逻辑大致与Move重合]

[以下略去方法的第一个对象参数]

[Force]

Force{[方向],[作用点]}

[作用点为close时,作用点为离受控角色最近的点,为center时,作用点为对象刚体重心]

[Meet]

[1 返回发射射线遇到的刚体]

A = Meet: {[方向],[发射距离],[可选发射起点]}

[2 返回是否碰到了期望的对象,它是1的基础上套一层判断逻辑]

Bool = ?Meet:{[方向],[发射距离],[可选发射起点],[可选期望对象名]}

[如果不填写期望名,那返回的将是是否碰到了任何对象]

[Find]

[依据条件全局寻找对象]

Find A.Name = * [在全局找到一个名为*的对象,引用为A]

Find All A.Name = *
[获取所有满足条件的对象命名为A,接下来对A的操作会对所有对象生效]

[执行语句的嵌套]

Move{From A to B , , Meet: { up , (From A to B).distance , B } }

[周期头部不可嵌套]

[生成语句]

generate A at (x,y,z)

[A可由Find得到,方向同样也一般由计算得到]

[协程]

[简单语句]

wait 5 [等待5秒再继续执行下面语句]

其他要实现的内容

[支持中文编程]
[这点也很有必要,理解容易,门槛低,也许对语法的抽象也能起到积极作用]
[支持全局变量和范围变量]

Name[对象名,同样是脚本名]
Name = * [表示将此对象名称改为* 并将此脚本内容添加到*对应内容之后 可用于应用预制体特性到此对象上 而改写预制体的名称为一个新名即创造了一个预制体的变体]

this 脚本引用
Object 当前对象
this.Object 脚本挂载的所有对象
Object.Script 从对象获取脚本

Camera 摄像机,作为对象
Mouse 鼠标,接收事件

Time [获取到游戏运行时间,表值]
Time + 2*3600 [时间前进2小时,具体做法是等待协程计算未来结果并返回]
TimeSpeed [实际是每秒多少次update]
TimeSpeed * 2 [时间加速两倍]
TimeSpeed * 0 [静止]

[方向计算]

From A to B [表示一个包含方向和距离的向量,使用时用括号包裹]
(From A to B).direction [归一,表方向]
(From A to B).distance [求距离]

[速度]

v(Object.v)
v.x v.y v.z

[执行语句]

Wait{[时间],[条件,为真时终止等待]}

[两个均可选,时间不写默认为始终等待,如果写了条件无论时间是多少触发了条件就会跳出等待,都不写即协程在此处终止,但也有可能在进行循环,请看下面的例子。]

[等待期间执行检测]

wait until{x > 10}

Wait{ , ?Meet:{north,10,,B}}

[Write]

[将一段语句写入特定脚本]

Find B.Name = *

Write “”” x = x + 2 “”” to B.Script

[将被三个引号包裹的语句写入名为*对象的脚本尾部,语句部分可换行]

[还可以指定写入的周期]

Write “”” x = x + 2 “”” to B.Script.Now

[封装函数书写]

Function: {
    FunctionName,{
    [首先认领参数,认领的次序为参数展示的次序,外面参数名里面要求类型]
    parameter(type),
    direction(Vector3),[From to等计算语句只会将结果传入这里,所以类型没错]
    moveType(string),
    moveTarget(string)
    [虽然参数会自动匹配,但建议可选的参数放在后面,避免参数顺序错乱]
    },{
    [接下来写具体逻辑]
    if(!moveTarget){
        moveTarget = Object
    }
    if (direction != null){
        if(moveType == "Instant"){
        moveTarget.x = moveTarget.x + direction.x
        moveTarget.y = moveTarget.y + direction.y
        moveTarget.z = moveTarget.z + direction.z
        }
        }
    }
}

[协程方法需要写两套,语法我之后再想吧]
[重写]
[这会很常用]
OverrideFunction:{Name,Para,newLogic}

[继承base添加]
[有熟悉原函数内容的必要]

OverrideFunction:{Name,Para,{Name:{Para},newLogic}}

[玩家自己写的对象局部封装函数需手动引入指定的function]

import Name.FunctionName

[需注意此脚本内则不可出现与方法同名的方法和变量名,重写函数当心覆盖]
[模块解析器内的方法则不用]


在这两天的脚本处理中,原本的语法规则做了一些修改来规避bug,减少复杂处理。

主要的bug出在文本处理上,层出不穷,最后只好一步一步地打印跟踪查找问题。

所以当前没有实现的假想可能会在方法构建中更改规则。

对于当前来说,既然抽象语法树已经构建,之后的处理也就是在原基础上进行扩展,我相信不会更难。


此方悬停
相册 小说 Ai