3.2 脚本系统

我们都知道目前在游戏中最常用的脚本系统是Lua,请分析除热更新支持外,使用脚本系统有何优劣?如果让你自己设计一套脚本系统,应该注意什么?

问题分析

首先明确什么是脚本。在Unity 3D中,我们使用的C#脚本并不属于脚本系统中的脚本范畴。这里的脚本的使用方式与字节码(ByteCode)类似,是在运行时通过虚拟机系统(Virtual Machine)来载入文件,动态更改逻辑影响游戏行为。从本质上讲,它是把行为控制部分从编码层转向了数据层。通过脚本系统,逻辑与行为更灵活地被外部数据控制,程序只负责执行。

在脚本系统中,一条命令被可优化的底层操作定义,一系列这样的命令被编码成字节流。虚拟机通过中间层依次执行命令。它的优点是可以灵活地定义行为,并动态改变逻辑。缺点也显而易见,由于需要VM的支持,因此这种模式的效率不可能太高。Lua就是这样的一种字节码,它有性能优异且轻量的VM,并提供多个平台的支持。有大量的商业游戏使用它作为脚本引擎语言。

魔兽世界UI结构

很多人知道Lua语言是从魔兽世界开始的。在魔兽世界的聊天中输入:

    /script print("Hello, WoW")

即可运行Lua脚本,输出“Hello WoW”。通过这样的方式,玩家几乎可以通过输入命令做游戏中的任何事。于是有很多人利用这点,制作了很多外挂,来实现自动化游戏,迫使暴雪公司不得不限制接口的功能。

与只在开发过程中使用的传统脚本系统不同,魔兽世界将脚本的控制权交给了玩家,使得每个人都可以自定义游戏。例如,通过下面的脚本可以关闭小地图:

    -- close minimap
    /script UIFrameFadeOut(Minimap,1)

    -- open minimap
    /script Minimap:SetAlpha(1)

通过几行脚本,就可以轻易地更改界面布局。我们也可以添加自己想要的功能,如监听游戏事件、弹出提醒框等。

由于MMO类型的游戏有着丰富多变的玩法,因此无论是游戏的开发,还是游戏的运行,引入脚本系统都可以大大提升游戏的灵活性。

Dota2角色的AI结构

对于系统或UI界面没那么复杂的竞技类游戏,脚本系统也有它存在的意义。DOTA2的AI就是这样一套脚本系统,从角色选择、装备合成、技能释放等初级的操作,到分路选择、团战时机判断、角色间配合等团队型决策,都可以通过Lua脚本自由定制。例如,截至本书编写时间,在DOTA2的7.x版本中,通过更改hero_selection.lua,即可控制对战电脑的阵容:

    function Think()
        if ( GetTeam()==TEAM_RADIANT )
        then
            print( "selecting radiant" );
            SelectHero( 0, "npc_dota_hero_antimage" );
            SelectHero( 1, "npc_dota_hero_lina" );
            SelectHero( 2, "npc_dota_hero_sven" );
            SelectHero( 3, "npc_dota_hero_bloodseeker" );
            SelectHero( 4, "npc_dota_hero_crystal_maiden" );
        elseif ( GetTeam()==TEAM_DIRE )
        then
            print( "selecting dire" );
            SelectHero( 5, "npc_dota_hero_drow_ranger" );
            SelectHero( 6, "npc_dota_hero_earthshaker" );
            SelectHero( 7, "npc_dota_hero_juggernaut" );
            SelectHero( 8, "npc_dota_hero_mirana" );
            SelectHero( 9, "npc_dota_hero_nevermore" );
        end
    end

如果想更改角色的装备合成,也可以重写每个角色自己的Lua控制文件。例如,要想更改“流浪剑客斯温”的出装,就需要重写item_purchase_sven.lua文件:

    local tableItemsToBuy={
        "item_tango",
        "item_clarity",
        "item_flask",
        ----------------------
        "item_blink",
        ---------------------
        "item_boots",
        "item_blades_of_attack",
        "item_blades_of_attack",
        ----------------------
        "item_ogre_axe",
        "item_quarterstaff",
        "item_sobi_mask",
        "item_robe",
        ----------------------
        "item_ogre_axe",
        "item_mithril_hammer",
        "item_recipe_black_king_bar",
        -----------------------
        "item_lifesteal",
        "item_mithril_hammer",
        "item_reaver",
        ----------------------
        "item_reaver",
        "item_vitality_booster",
        "item_recipe_heart",
    };

    // PurchaseLogic
    //......

可以看到,通过更改角色的出装表,定制购买道具的顺序,即可个性化地控制角色出装。不做AI的深入讨论,有兴趣的读者可以自行上网查阅相关资料。

在DOTA2的套脚本架构中,最值得借鉴的是,它的主逻辑并不是使用Lua制作的,而是使用效果更高的C++编写的。对于不需要定制的通用功能,采用高效的实现方式是明智之举。如果需要定制角色行为,就创建一个以角色名为后缀的Lua文件。例如,对于角色Lina,可以创建item_purchase_Lina、ability_item_usage_Lina.lua等。在加载游戏时,会检测相应脚本,如果存在名称匹配的脚本,就将其载入运行对应逻辑,否则使用默认的实现,最大化地实现性能的优化。

自设计脚本

既然Lua这么强大,是否就不用自己编写脚本系统了呢?理论上说,Lua可以应付各种情况,没必要自己写一个解释器,重新造一个“轮子”。但这里有个脚本开发的友好性问题。使用脚本的人不一定是熟悉程序的人,因此能正确地编写Lua脚本是有一定门槛的。如果是自定义的脚本,就可以将语法设计得更具体一些,贴近自然语言,以降低使用门槛。值得强调的是,设计脚本语法并不是没有成本的,我们在动手之前,需要衡量工作量与工期。下面就以CG动画脚本为例来说明。

在游戏中制作CG过场动画时通常需要策划和美术多人协助完成。对于Unity 3D来说,可以找到制作CG事件的插件,但从实际推行的效果来看,学习成本并不低。对于常规的场景分镜,其实并不一定要有特殊的动画效果,因此采用有限语法的脚本完全可以满足需求。在制定语法时,完全可以使用中文,以降低英语门槛,防止出现拼写等错误。一个简单的脚本可以是下面这样的。

    场景:3-1
    描述:两人对于需求更改的争执
    角色:程序员[替代标志(A),位置(2,2,3)],策划人员[替代标志(B),位置(2,2, -4)]

    开始
    摄像机:过肩拍[目标(B),距离(10)]
    摄像机:淡入[时长(1)]
    移动:B[位置(2,2,0)]
    等待
    转向:A[目标(B)]
    下一段
    摄像机:回应
    对话:B[文字(已经开始做宠物模块了吗?)]
    对话:A[文字(刚刚制作好界面,都快累吐血了)]
    对话:B[文字(那真是不太好,由于之前的界面感觉不对,现在已经有新版了)]
    对话:A[文字(没时间重做了,这版先这样吧,我先写功能)]
    对话:B[文字(恩,也行。这个新版感觉也不太对)]
    对话:A[文字(我就知道...)]
    摄像机:淡出[时长(1)]

    下一段
    屏显:文字(一周之后),时长(2)
    摄像机:淡入[时长(1)]
    对话:B[文字(告诉你个好消息,宠物模块用处不大,这个功能可以去掉了)]
    摄像机:仰拍[目标(A),距离(5)]
    对话:A[文字(我刚做完!)]

    结束:进入战斗

通过上面的脚本定义方式,能够兼顾可读性与实用性。实际应用中,可能会比这个脚本的功能参数更多,例如播放声音、特效等。但总体来讲,纯文本的内容易于修改,而简明的语法可以使得CG内容更直观。为了避免出错,我们甚至可以编写一个语法校验器,来检查是否在特定位置出现非关键字。VM方面,每行是一条命令,中括号中是参数,小括号中是参数值。在运行时,每次读入一行,并转义成对应的逻辑执行。其中需要约定一些关键值,比如摄像机的回应模式,是指两个对话的人依次出现在屏幕的中心,并且脸部朝向相对,效果可以参照新闻采访。诸如此类的定义都需要结合项目实际需求制定。

总结

通过分析脚本系统的内在原理,我们分析了魔兽世界与DOTA2对应系统的优劣,也探讨了是否有必要设计自己的脚本语法。虽然目前大多数游戏使用脚本系统是为了能够热更新,但相信随着脚本系统的广泛应用,它会因自身的优势被用于更多的场景。