场景模型进阶-逻辑与脚本

DinS          Written on 2018/1/17

在掌握了Urho3D场景模型基础知识后,让我们深入研究一下场景模型。
要实现的目标:点击按钮,在一片区域中随机生成box并在场景中显示。
额外要求:将逻辑嵌入场景中。

这是一个更贴近实际游戏需求的任务,事实上有几种方式来达到目标。

一、使用Component

在初次介绍场景模型时,我们就说过Component用于装载具体的模型和逻辑,当时我们仅仅展示了静态模型,现在让我们尝试把逻辑放入Component中。

当然第一件事是制作逻辑部分的代码,我们采用C++11的random库来解决。我们自己写个类来实现生成随机坐标,这部分略去。
然后仅仅这样还不够,我们需要从Urho3D提供的LogicComponent基类派生出我们自定义的逻辑类才可以嵌入,以下是.h部分代码:

固定写法,一个public一个构造函数传context。其他的东西就是正常的类,该怎么写就怎么写。看.cpp:

大图点这里

构造的时候把LogicComponent初始化掉,剩下的都是常规,这里不讨论随机数生成器。语义上来说就是x在区间(-30,30),z在区间(0,60)。
为什么是这样的区间?稍后解答。

至此类就完成了,看看如何将其加入场景。
关于头文件和相关设置的部分看之前章节,这里认为已经都关联好了。
继续之前的代码,在Start()里设置场景部分增加如下代码。

大图点这里

就两行。
第一行是在场景中增加一个Node,用于放将来的box。注意构造参数。之前都只给出了名字,这里后面还有两个,REPLICATED跟网络有关,在这里用处不大,仅仅是因为其在ID之前所以必须设置一下,默认就是这个。最后那个参数重要,是设置node或者component的ID。之后我们将通过ID获得相关对象。
第二行是在node中创建了一个component,类型就是我们写的类。
之后是注册事件,这个参考之前的专题
实现事件如下:

大图点这里

注意第一行,我们通过ID直接获取对应node。
如果你还记得我们是怎样旋转中间的box的:

大图点这里

我们使用了GetChild,用名字来获取。区别在哪里?
用名字获取涉及字符串比较,如果再有递归深入效率会很低。用ID则是直接获取,效率高。当然这里的写法仅仅是展示这样一个功能,更好地方法是用枚举来定ID。
另一个区别是component没有名字,但可以有ID,所以可以直接用ID获取component。

接下来我们使用GetComponent获取逻辑组件。注意这里并没有什么名字或ID,GetComponent使用类型来获取对应组件。所以不要在一个node里新建相同类型的组件,这是自找麻烦。
如果不从LogicComponent派生,这里GetComponent会报错。
这里之所以先获取node再通过node获取component是为了展示功能。

获取component后就可以调接口了,这里生成随机坐标,然后根据坐标放置box。

这样一来我们就把逻辑放入了场景中,这样有助于实现OOP。
运行一下看效果(点击多次):

可以看出是随机放置的。
现在回过头来说说x和z的范围问题,顺便讲讲3D坐标。Urho3D的三维坐标可以这样表示:

也就是说x是横向,y是纵向,z是深入屏幕。
原点在哪里?大概是这里:

这解释了范围问题:

我们指定y是-5。这样所有的box都在一个水平面,略低于视线。
x如果是0就是正中央,-30到30则是在屏幕左右移动。
z如果是负数就看不到了,指定0到60就是深入屏幕一定距离。
综上所述,在这个范围我们最容易看到效果。

使用component有利有弊。
优点在于与场景结合紧密,缺点是必须从LogicComponent派生。
固然可以最后加一个LogicComponent的壳,不过一般OOP都会涉及继承,在来一个LogicComponent就是多重继承,要慎重。

二、使用脚本

使用脚本进行逻辑控制应该是更加好的方式,因为其更加灵活,而且相比上面介绍的也更加简单。
Urho3D官方推荐使用AngelScript做脚本,本专题遵循官方的建议。AS本身就有许多说道,详见另一个专题《AngelScript框架介绍》。
Urho3D在编译库时本身就有一个选项:AngelScript Integration。实质上Urho3D包装了AS的使用,更加方便调用。更具体的说,就是在脚本引擎中先注册了各种符号。本例使用Urho3D提供的一个脚本来说明。
先来看看这个脚本:

大图点这里

麻雀虽小五脏俱全,这个脚本足够说明许多问题。
第一,AS的脚本语法与c++非常相近,可以按照c++的写法来写脚本。当然有一点除外,脚本里没有指针,也用不到指针。
第二,这里的脚本直接声明了一个类,最后没有;。这个类继承自ScriptObject。这并非必须,不过如果你想把这个类放入Component里并实例化,则必须继承。
第三,声明了一个成员变量。这个Vector3之所以能够被脚本识别,是因为Urho3D已经在脚本引擎里注册了符号。如果想用自己的类,见AS专题《AngelScript进阶用法:在脚本中使用代码类》。
第四,成员函数。与c++一样,不过默认为public。参数里有一个&in,这是AS特殊标识符,类似于const引用防止传参拷贝。
第五,Update。这是一个特殊的函数,如果在类里面声明了Update,则程序每次执行Update事件时会自动调用该函数。同理还有其他函数,详见https://urho3d.github.io/documentation/1.7/_scripting.html。
第六,node。这个node是写脚本的一个shortcut。因为如果声明了脚本类且继承自ScriptObject,通常会放到某个node的component中。这样一来使用node来指代那个node,可以方便脚本书写。

脚本就是这样了,从语义上来说该脚本的作用是每次Update时旋转一个node,旋转参数通过SetRotationSpeed和timeStep确定。
接下来看看如何调用脚本,为了调用脚本,第一件事是注册脚本子系统:

大图点这里

通常放在构造函数中。
个人估计其执行的就是许多许多注册语句。脚本子系统默认不启动,因为有些游戏不需要脚本,调起来浪费资源。启动后就可以调用了。

大图点这里

添加box与之前的一样,重点看脚本逻辑。
我们首先在boxNode_节点下新建了一个Component,类型是ScriptInstance,从名字可以看出这里将放置脚本类的实例。然后是该instance调用CreateObject,指定要读取的脚本,以及脚本中的类。至此脚本类已经实例化并进入程序了,非常便捷。

下面展示了如何调用成员函数并传参。
传参统一使用VariantVector,压入顺序依次是参数顺序,使用Variant表明可以是不同类型的参数。然后是调用instance的Execute执行一个函数。通过函数声明确定脚本中的对应函数,然后指定参数。仅从代码上看,跟调用了一个全局函数类似,这个就不必管了。

这里还有一个小问题:如何知道脚本中的类名和函数名?
方法有几种,或者强制约定,或者通过配置文件,反正都是字符串。或者操作底层的脚本引擎来获取metadata。

另一个Update函数呢?这个会被自动调用。为了展示效果,让我们把代码中的注册事件注释掉:

也就是说,如果脚本不执行,那么中间的box应该不会旋转才对。
现在让我们执行看看:

在旋转,说明脚本执行成功了!

回顾一下,可以发现用脚本执行游戏逻辑确实更加好。
只要写个脚本,然后几行代码调用成员函数即可。
如果使用原始的AS,会有些复杂,不过Urho3D替我们做了很多工作,所以用起来很简便。当然这个简便是指仅仅使用Urho3D提供的各个符号,如果自定义类和函数,当然还需要了解AS的使用方法。

最后说一说脚本的其他注意事项,具体说明和写法详见https://urho3d.github.io/documentation/1.7/_scripting.html

脚本可以执行Delayed method calls,指定一段时间后再执行某些功能。这个是在脚本内部完成的,有固定套路。
可以直接从脚本执行全局函数,这样就不用实例化脚本类,这种调用更加简洁,只不过不能放到Component里了。
脚本可以预先编译,这样执行起来更加快速,也起到加密效果。如何加密可以使用Urho3D提供的tools完成。
在Urho3D里,AS的脚本语言并不跟原始的AS完全一致,有一些小调整,不过影响有限,一般来说用不到吧。

三、综合评价

Urho3D提供了在component里放入逻辑代码和脚本的功能,对于游戏开发还是很便利的,不过如果深入研究,应该意识到这样做存在一些隐患。
现在流行MVC框架,其核心观点就是界面、逻辑、数据的分离,方便后期维护扩展,但是按照Urho3D提供的这种方法,虽然界面和逻辑并不是直接放到一起了,但是也不能算分开。
说不是直接放到一起,是因为component相对于界面的node而言是组合方式,有一定独立变化的能力。
说不算分开,理由也是充分的,每个node有自己的component,是关联的。
所以也可以考虑单独做一个逻辑中心接口,然后component去调这个接口,真正的逻辑在另外的部分实现。至于到底如何才算是最佳实践,只能程序员自行决定了。

推荐优先使用AS脚本制作游戏逻辑。


逻辑部分说完了,让我们再深入研究研究UI,见《UI研究》。