游戏开发论坛

 找回密码
 立即注册
搜索
查看: 8661|回复: 18

底层脚本、高层脚本和微线程

[复制链接]

8

主题

34

帖子

44

积分

注册会员

Rank: 2

积分
44
发表于 2005-10-25 11:19:00 | 显示全部楼层 |阅读模式
    脚本在游戏中占据的位置是不可质疑的。不仅仅是由于其解释语言的特性(修改不需要对源代码进行编译);而且游戏的逻辑采用脚本语言更加容易描述。对于底层脚本和高层脚本,至今尚未有一个明确的定义。本系列文章试图从技术的角度,在概念上对底层和高层脚本进行一个划分,并对微线程在游戏引擎的引入的必要性进行分析。
以下面网上对于脚本与游戏主循环的同步作为一个引子,展开本文研究的话题。
    “基于命令的脚本语言可以用来控制游戏中的non-player character,可以让他们"自主"的转悠、动作,使游戏更生动。当然,这些NPCs的动作也可以使用Hard-code的方式跟游戏引擎写在同一处,显而易见的是,这种方式将游戏的底层(Gameengine)和游戏的高层(Logic)混在一起,对于游戏的扩展、游戏的调试。。。。没有可取之处。
    下面的这些脚本片断用来控制一个NPC,每个命令的含义很直观。
// RPG NPC Script
// A Command-Based Language Demo
// ---- Walking in a square pattern
ShowTextBox "THAT WAS SIMPLE ENOUGH."
Pause 2400
ShowTextBox "NOW LET'S WALK IN A SQUARE PATTERN."
Pause 2400
HideTextBox
Pause 800
SetCharDir "Right"
MoveChar 40 0
MoveChar 8 8
SetCharDir "Down"
MoveChar 0 80
MoveChar -8 8
    要让这个脚本控制一个NPC,我们必须编写一个"脚本解释器",从脚本文件内读入每行命令、解释、并执行。需要注意的是,如何与游戏的主循环同步呢?也就是说,如果我们顺序执行这个脚本序列(在一个循环中),则整个程序的其他部分得不到响应,那就会出现这样的情况:在NPC运动过程中,游戏主角将不会响应用户的控制。如何解决这个问题呢?多线程(Multi-Thread)?这是个自找麻烦的做法。
    实际上,为了解决面临的同步问题,我们可以把脚本的作用限制在"改变程序的状态"这个范围内,例如:NPC要从A处移动到B处,我们不会在解释这个命令脚本的时候做这样的事情:把代表NPC的图片从A处以一个微小的增量移动,直到它到到B处,这样就导致了"不同步",我们要做的是,设置这个NPC的状态为正在移动,并记录目标点的坐标,然后,在游戏的主循环中,我们检测到这个NPC的状态,如果它没有到目标点,我们就继续移动它。
同时需要注意的是,如果这个命令没有执行完,那么我们应该不允许下一个命令的开始,也就是说,我们在游戏的主循环中,依次解释每一条命令,并且设置相应的状态值,但是遇到类似"移动"这样的命令,如果没有执行完这条命令,程序将不会解释执行下一条命令,也就是说,移动到某处不是一蹴而就的,是需要过程的。”

    本文将针对六个方面的问题展开讨论:高层脚本和底层脚本;微线程;微线程与主循环同步;脚本组织与对象脚本;脚本与任务系统;网游客户端脚本与游戏的交互性。”
   
  本文内容仅代表星河工作室针对此类问题的观点。相关的技术在星河平台2.1版本全部得到支持(SRPV2.1),可访问相关网站:http://www.srplab.com。

一:高层脚本和底层脚本

    高层脚本更适于描述逻辑,上述的例子是一个高层脚本,下面的一段描述也是高层脚本:
走到(死水沼泽,56,99)
等待(500)毫秒
走到(死水沼泽,56,42)
找到(首饰店掌柜)(死水沼泽【7】,52,31)
与首饰店掌柜对话
选择【出售首饰】
自动卖掉【手镯】类物品
自动卖掉【戒指】类物品
自动卖掉【项链】类物品
选择【返回】
结束对话
找到【服装店掌柜】(死水沼泽【7】,49,37)
与【服装店掌柜】对话
选择【出售衣服】
自动卖掉【衣服】类别物品
自动卖掉【头盔】类别物品
选择【返回】
结束对话
    对于底层脚本,更加类似于函数,如下一段代码(摘自:Game Programming With Python,lua and Ruby)是底层脚本:
function render_frame(screen, background)
-- When called renders a new frame.
        -- First clears the screen
        SDL.SDL_FillRect(screen, NULL, background);
        -- re-draws each actor in gamestate.actors
        for i = 1, getn(gamestate.actors) do
                gamestate.actors:render(screen)
        end
        -- updates
        SDL.SDL_UpdateRect(screen, 0, 0, 0, 0)
end
function engine_init(argv)
        local width, height;
        local video_bpp;
        local videoflags;
        videoflags = SDL.bit_or(SDL.SDL_HWSURFACE, SDL.SDL_ANYFORMAT)
        width = 800
        height = 600
        video_bpp = 16
        -- Set video mode
        gamestate.screen = SDL.SDL_SetVideoMode(width, height, video_bpp, videoflags);
        gamestate.background = SDL.SDL_MapRGB(gamestate.screen.format, 0, 0, 0);
        SDL.SDL_ShowCursor(0)
        -- initialize the timer/ticks
        gamestate.begin_time = SDL.SDL_GetTicks();
        gamestate.last_update_ticks = gamestate.begin_time;
end

    那么什么是高层脚本,什么是底层脚本,两者之间的区别是什么?
    如果脚本不能够顺序执行完毕,则该脚本是一个高层脚本。高层脚本更加适合于描述游戏逻辑。如果脚本能够顺序执行完毕,则该脚本是一个底层脚本,底层脚本使游戏在更新局部功能的时候不需要重新编译。
对于高层脚本描述游戏逻辑,有时一条脚本命令需要一个执行过程,比如:走到(死水沼泽,56,99),该脚本命令执行后,会触发一个移动的动作,需要几帧甚至更多的游戏循环,才能够执行完毕。类似命令“MoveChar 0 80”同样。因此在命令执行过程中,需要让出CPU,给游戏其它部分运行,直到达到目标状态。
对于底层脚本,不存在类似的问题,执行过程都是顺序的,中间不中断。
高层脚本执行如何进行,采用类似前面,通过引入状态的方法进行控制,该方法有哪些缺陷?有没有更好的方案,答案是有,哪就是采用微线程。
我们在第二部分讨论微线程,并与一个通过引入状态进行控制的代码进行比较。
abc

8

主题

239

帖子

239

积分

中级会员

Rank: 3Rank: 3

积分
239
发表于 2005-10-25 12:51:00 | 显示全部楼层

Re:底层脚本、高层脚本和微线程

等看微线程,没搞过

8

主题

34

帖子

44

积分

注册会员

Rank: 2

积分
44
 楼主| 发表于 2005-10-26 10:31:00 | 显示全部楼层

底层脚本、高层脚本和微线程(二)

二:微线程

该节采用一个引自《游戏编程精粹2》的例子作为开篇。
Void Janitor:rocess(){
    While( true ){
        GameObject *Target = GetNewTarget(this);
        While( Distance( this, Target ) > k_collisionTolerance ){
            waitOnFrame();
            MoveABitTowards( this, target );
        }
        Dispose( this, Target );
    }
}
在上述代码中,存在一个死循环,采用传统方式运行,CPU会被上述代码一直占据,游戏的其它部分得不到执行。通过引入状态,采用状态机可以解决该问题,其代码如下:
Janitor::Janitor() : m_target(0),m_state(k_NeedsGoal){
}
Void Janitor::ProcessFrame(){
    Switch( m_state ){
    Case k_NeedsGoal :
        M_target = GetNewTarget( this );
        If( Distance( this, m_target ) <= k_collisionTolerance ){
             M_state = k_DisposeObject;
             ProcessFrame();
             Return;
            }
            M_state = k_MovingTowardsTarget :
         Case k_MovinfTowardsTarget :
              MoveABitsTowards( this, m_target );
              If( Distance( this, m_target ) > k_collisionTolearance )
                  Break;
              Else
                  M_state = k_DisposeObject;
        Case k_DisposeObject:
            Dispose( this, m_target );
            M_state = k_NeedGoal;
            Bresk;
        }
}
上述代码不再简单精致,而是成了一个庞大,难读的状态机。同时由于需要m_state和m_target,该状态机不能被其他类重用。这就是采用状态解决高层脚本问题的严重缺陷,程序结构凌乱,不易于维护;当高层脚本数量多的时候,比如达到几万的时候,复杂性很难想象。
解决问题的方法是启用线程,将前面的代码作为一个线程运行,这样,当调用函数waitOnFrame时,就可以切换到下一个线程。但是,采用线程的思路是好的,但是实际是行不通的,这存在多方面的原因:多个线程之间临界区访问的同步,增加了程序的复杂性;线程创建的数目,在目前的操作系统上都有一定限制,Windows2000约2000个左右,并且每个线程至少消耗8KB的内存,想象一个网络游戏的服务器端,存在大量的NPC和其它任务,可能需要几万甚至更多不能够顺序执行的代码,采用线程基本是不可能的。可以采用纤程,但是堆栈占据的空间将是很大的开销。
为解决上述问题,引入了微线程的概念,通过切换堆栈和指令执行的指针,在一个线程内,实现多个代码段并发执行。微线程在操作系统级别上是感知不到的。
切换堆栈和指令执行地址的代码很简单,采用几条汇编既可以完成,如下:
Push ebx
Push ebp
Push esi
Push edi
Mov eax, esp
Mov esp, s_globalStackPointer
Mov s_globalStackPointer, eax
Pop edi
Pop esi
Pop ebp
Pop ebx
Ret

实际实现的时候,需要增加对微线程的管理,微线程的结构化异常处理。在SRP平台中,增加了如下的管理功能。
1.        微线程的创建,删除
2.        微线程的状态管理:运行,就绪,挂起,阻塞。
3.        微线程的挂起,恢复,睡眠
4.        微线程等待事件(触发器),微线程之间消息收发。
SRP平台实现的微线程,同时支持C函数和Lua脚本

void MiniTaskFunction1()
{
    int n = 0;

    while(1 ){
        printf( "MiniTaskFunction1 ----Suspend  Times = %d\n",n );
        n++;
        MiniTaskManager_SleepMiniTask( 50 );
        printf( "MiniTaskFunction1 ----Resume \n" );
    }
}

Main()
{
    InitMiniTaskManager();
     
    //-------------创建10000个微线程
    for( i=0; i < 10000; i++ ){
        TaskID = MiniTaskManager_CreateMiniTask("Function1",(char *)MiniTaskFunction1);
        MiniTaskManager_ResumeMiniTask( TaskID );
}
//-------------采用Lua脚本创建微线程
TaskID=MiniTaskManager_CreateMiniTask("Function2", "print(\" Lua test 1000\")");
MiniTaskManager_ResumeMiniTask( TaskID );
}

6

主题

390

帖子

400

积分

中级会员

Rank: 3Rank: 3

积分
400
发表于 2005-10-26 12:43:00 | 显示全部楼层

Re:底层脚本、高层脚本和微线程

关于微线程自身的存储和恢复,比方我需要Load/Save的时候,有没有处理办法?

197

主题

1041

帖子

1104

积分

金牌会员

Rank: 6Rank: 6

积分
1104
QQ
发表于 2005-10-26 13:37:00 | 显示全部楼层

Re:底层脚本、高层脚本和微线程

回楼上~load/save state标志即可

mini中处理最多的不是frame~是action
while (timer)
while (frame->......)
{
action->start......
action->event............
action->stop
}
end;

23

主题

515

帖子

552

积分

高级会员

Rank: 4

积分
552
发表于 2005-10-26 18:30:00 | 显示全部楼层

Re:底层脚本、高层脚本和微线程

赞一个,接下来将是脚本时代。

8

主题

34

帖子

44

积分

注册会员

Rank: 2

积分
44
 楼主| 发表于 2005-10-28 10:20:00 | 显示全部楼层

Re: 底层脚本、高层脚本和微线程

三:采用微线程与主循环同步

对于前面通过状态进行同步的方法“设置这个NPC的状态为正在移动,并记录目标点的坐标,然后,在游戏的主循环中,我们检测到这个NPC的状态,如果它没有到目标点,我们就继续移动它。
同时需要注意的是,如果这个命令没有执行完,那么我们应该不允许下一个命令的开始,也就是说,我们在游戏的主循环中,依次解释每一条命令,并且设置相应的状态值”这种方法不太可取,对于小游戏,或许可能简单一些;对于大型游戏,需要执行的高层脚本非常多,如果采用状态方法进行控制,都要在主循环中进行判断,那么程序的结构非常凌乱,可扩性不强。另外将会影响游戏的交互性(客户端自定义脚本,不容易实现)。
采用状态的方法控制脚本的执行,都是按照一行一行的命令方式,当脚本中有分支,循环时,函数调用时,处理起来非常的复杂。目前成熟的脚本语言:Lua、Python和Ruby等,都不支持类似的执行控制,因此没有办法采用,需要自行开发脚本语言,工作量和难度可想而知。Lua中有微线程,但是使用非常受限制,不支持C/脚本混合的处理。
对于上述的问题,采用本文提出的微线程则是比较好的解决方法,系统中可以支持几万个微线程运行,能够满足游戏的需要,在主循环中,对微线程进行调度。
    while( 1 )
    {
        if( PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ) )
        {
            if( !GetMessage( &msg, NULL, 0, 0 ) )
            {
                return msg.wParam;
            }
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else
        {
            ReceiveGameMessages();
            if( bIsActive )
            {
                 MiniTaskManager_PeriodSchedule();
                UpdateFrame();
            }
        }
    }
对于一个高层脚本,在微线程的环境下运行,例如,在SRP平台Server端,可以用微线程的方式,运行下面的脚本:
aaa = 0
while( aaa < 10 ) do
    SRPObj.PrintMsg( "aaa = ", aaa )
    SRPObj.SleepMiniTask( 100 );
    aaa = aaa + 1;
end

当代码执行到SleepMiniTask,调度会将当前的微线程挂起,让出CPU给其它部分进行处理。游戏主循环会对微线程进行调度,对于Ready状态的微线程,将被调度执行。

50

主题

992

帖子

1012

积分

金牌会员

Rank: 6Rank: 6

积分
1012
发表于 2005-10-28 12:57:00 | 显示全部楼层

Re:底层脚本、高层脚本和微线程

好文

14

主题

131

帖子

136

积分

注册会员

Rank: 2

积分
136
发表于 2005-10-30 14:07:00 | 显示全部楼层

Re:底层脚本、高层脚本和微线程

精彩

8

主题

34

帖子

44

积分

注册会员

Rank: 2

积分
44
 楼主| 发表于 2005-11-2 11:57:00 | 显示全部楼层

Re: 底层脚本、高层脚本和微线程

底层脚本、高层脚本和微线程(四)

四:脚本组织与对象脚本

使用脚本便于在线修改,比如“受到攻击,根据武器的伤害,角色的防御能力,计算角色HP下降的点数”,为了游戏的平衡,该判断中的各个数值可能需要经常调整。脚本也可以定义个性化的界面或者操作,通过修改脚本,定制不同的显示/操作方式;另外对于语义模糊场合,语义可能经常变化,比如:“好”,“坏”,也适用于采用脚本实现。但是脚本的执行速度和C/C++代码存在量级上的差异,因此什么情况下使用脚本,需要仔细斟酌。
每个角色都可能相关多个脚本,比如:npc_a_talk;npc_a_offer_money等,这些可以按照角色组织成为一个文件,也可以放在不同的文件中,当需要时,加载执行相应的脚本。对于脚本的组织,与各个游戏具体相关,没有统一的原则,我们在这里主要介绍一下SRP平台对于对象脚本的组织,供各位参考。
SRP平台是分布式平台,脚本的保存/装载,全部由SRP平台自动完成。此外,自动实现了脚本在服务器和客户端的同步,当需要修改脚本时,只需要修改服务器端,客户端对应脚本的修改,由SRP平台实现。调试器内置了对象的脚本编辑。SRP平台将脚本分为三类,内嵌脚本、独立事件脚本和命名脚本。
1.内嵌脚本。在C/C++实现的功能和事件的处理函数中,执行一些脚本片断,以便实现对功能和事件处理的部分逻辑进行修改。简单示例如:
void Object_Display(void *Object)
{
    char *ScriptStr;
    lua_State *L;
DWORD Width;

    ScriptStr =  GetFunctionScript ( ServiceModuleContext, Object, DisplayFuncID );
    //-- Display 为函数的UUID
    L = GetLuaState (ServiceModuleContext);
    DoBuffer ( L, ScriptStr, strlen(ScriptBuf) );
    Width = LuaToNumber ( L, -1 )
    LuaPop ( L, 1);
    DrawItem( Object, Width);  //  ---绘制
}

脚本:
“return 123”
该脚本与对应的功能或者事件对象相关联。

2.独立事件脚本。对于角色事件处理,可以独立采用脚本实现,不需要内嵌到C/C++代码中进行调用。当对应的事件产生时,由SRP平台负责执行对应事件的脚本。比如:
Button_OnClick事件,其脚本为:
“print( "按钮OnClient Event Was Triger " )”
当按钮被点击时,会在显示窗口打印字符串。
该脚本与对应的事件对象相关联。

3.命名脚本。独立使用脚本定义的功能,命名脚本可以分为一般执行或者微线程执行。在SRP平台中,任何对象都可以创建多个命名脚本。
命名脚本可以在Lua脚本中调用,也可以在C/C++代码中调用。在Lua脚本中使用下面的函数:
SRPObj. LuaDoObjectNameScript(Context,Object,ScriptName)
类似前面提到的npc_a_talk;npc_a_offer_money,都可以在SRP平台中作为命名脚本进行管理。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2026-1-22 19:42

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表