游戏的语言虚幻的脚本

发表于2015-07-08
评论0 790浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594

一、        脚本介绍

虚幻自己定义了一套 面向游戏 的开发语言 US ( ,不仅保留了传统c++的大部分功能。 同时在其上实现了很多新的功能,如,状态,网络同步,事件,委托,垃圾管理(c#中的特性),等等。 同时使用开发环境,可以很方便的对虚幻的脚本进行调试,跟踪。

 

游戏的主要逻辑 可以都通过脚本来实现。

 

脚本的数据驱动方式,可以为编辑器提供更灵活的支持。比如在脚本中,新增的对象类型,可以即时同步到编辑器中。在脚本的基础语法中,可以通过加关键字来定制,该函数是在Server端运行还是在Client端运行,同时uc中提供了大量的基础对象类型,并提供了良好的拓展架构。

 

虚幻脚本在设计之初,并未考虑效率。 (这点未来需要一个详细的效率分析数据)

 

本文从虚幻脚本的实现机制上做分析,并不涉及虚幻脚本语言的语法

 

二、        脚本对象

虚幻的脚本文件 UC为后缀,一个UC文件,对应一个脚本类。打开文件,会看到很多类java的语法。 主程序启动后,对这些UC文件做检查,如果有更新,则重新编译,编译后生成 *.u 文件。在程序或编辑器启动后,会加载这些 *u 件,读取脚本的代码,运行时 通过事件的方式,驱动脚本 解释执行。 其整个实现,是构筑在C++语言基础之上的。

 

1.         脚本对象分类

既然脚本是用C++实现的,那么我们首先要了解,实现脚本系统的 对象有哪些。

 

虚幻脚本,语法中的类,在C++底层实现时。用UClass表示. UClass 的继承关系UClass-> UState->UStruct->UField->UObject

类似的还有

UState         代表Unreal  脚本语法中的              状态

UFunction   **** 函数

UEnum         **** 枚举

Uconst :        *** 常量

 

顺便提以下,虚幻中所有的对象,都是从UnObject 中继承,因此UObject显得很臃肿。

             

下图是 实现脚本系统的对象的类图

          http://top.oa.com/pictures/201209/1346925651_8.png

 

2.         脚本对象属性

脚本对象属性 == 变量类型,就是描述对象的类型。

 

比如说 Unreal 语句 

bool  bValue ;  bool 是变量类型

Unreal noxss 的底层实现,用UBoolProperty来描述 bool 变量类型。类似的还有

UIntProperty UClassProperty……

      

       下面是脚本对象属性的类图

 http://top.oa.com/pictures/201209/1346925668_3.png

 

3.         脚本对象组织

脚本对向都是继承自UField类,下面我们来进一步分析一下脚本对象的内容。

 

       UClass为例:

 

UClassnoxssText中保存着脚本的代码;

 

navtive cpptext 字样的本地类. C++代码 保存在 CppText中。 (之前忘了说,Unreal 脚本是支持内嵌 C++ 语句的。)

 

noxssText Cpptext 都是UTextBuffer* 类型。

 

UClass 有个成员变量 UField * Children

UField  有个成员变量 UField  *Next ,

这个结构看起来像:

Children->Next->Next->……

         http://top.oa.com/pictures/201209/1346925685_88.png

下面是一段脚本的代码:

class SampTest extends Actor;

var int var1;

var int var2;

function PostBeginPlay()

{

  var1 = AddNumbers(1,2);

  var2 = AddNumbers(var1,3);

   GotoState('State1');

}

function int AddNumbers(int arg1, int arg2)

{

  local int result;

 

  result = arg1 + arg2;

  return result;

}

state State1

{

  function somestatefunction()

  {

     var1 = 0;

  }

  begin:

  var2 = 5;

}

state State2

{

  function somestatefunction()

  {

    var1 = 1;

  }

  begin:

  var2 = 7;

}

代码中有 1UClass对象,两个UProperty对象  4UFunction对象, 两个UState对象。 这段代码,在程序实现时。中如何组织呢  请看下图

http://top.oa.com/pictures/201209/1346925699_7.png

 

下图是对应的层级关系描述

http://top.oa.com/pictures/201209/1346925806_23.png 

上图中一个UState 的层级关系描述

http://top.oa.com/pictures/201209/1346925849_84.png

上图中一个UFunction的层级关系描述

http://top.oa.com/pictures/201209/1346925862_37.png 

 

  

 

三、         脚本的执行

执行过程中 所用到的对象

      UObject           封装了脚本中 基本的执行指令函数

      FFrame      维护脚本代码棧, 驱动指令执行

      GNatives    维护 脚本二进制指令,和 基本执行指令函数的 映射

      Event         用来调用脚本函数,驱动脚本执行的代码

1.   UObject

下图:上面对象的类图

http://top.oa.com/pictures/201209/1346925886_72.png

脚本的基本执行调用流程:

http://top.oa.com/pictures/201209/1346925898_39.png

AActor继承自UObject UStruct 中包含着被编译后的二进制脚本,执行指令也在其中。UObject::exe****() 字样的函数有300多个,这些函数,其实就是脚本指令的C++映射代码。我们可以通过扩展UObject,来增加自己的脚本指令。 

    void UObject::execXXX(FFrame& Stack, RESULT_DECL);

#define DECLARE_FUNCTION(func) void func( FFrame& TheStack, RESULT_DECL );

 

2.   GNative

通过 GNative 实现  EX_XXX 指令到 execXXX 代码的映射, 下面的代码是具体实现方法 

BYTE GRegisterNative( INT iNative, const Native& Func )

{

    static int Initialized = 0;

    if( !Initialized )

    {

        Initialized = 1;

        for( DWORD i=0; i<ARRAY_COUNT(GNatives); i++ )

            GNatives[i] = &UObject::execUndefined;

    }

    if( iNative != INDEX_NONE )

    {

        if( iNative<0 || (DWORD)iNative>ARRAY_COUNT(GNatives) ||GNatives[iNative]!=&UObject::execUndefined)

            GNativeDuplicate = iNative;

        GNatives[iNative] = Func;

    }

    return 0;

}

 

 

函数GRegisterNative()的调用,被封装在IMPLEMENT_FUNCTION 宏中。在Unnoxss.h中有该宏的定义:

#define IMPLEMENT_FUNCTION(cls,num,func)

    extern "C" { Native int##cls##func = (Native)&cls::func; }

    static BYTE cls##func##Temp = GRegisterNative( num, int##cls##func );

 

 

3.   Frame class

FFrame 中有个很重要的函数,Frame::Step() ,FFrame 使用这个函数进行 指令执行,并管理脚本函数棧。 FFrame的成员变量Code ( byte类型的指针) ,用来表示当前指令。成员变量Object (UObject类型的指针) 用来指定,当前处理的是哪一个对象的脚本。成员变量CodeOffSet,是UObject字节码的偏移量。 FFrame的构造函数

inline FFrame::FFrame( UObject* InObject, UStruct* InNode, INT CodeOffset, void* InLocals,FFrame* InPreviousFrame )

    :Node(InNode)

    ,Object(InObject)

    ,Code(&InNode->noxss(CodeOffset))

    ,Locals ((BYTE*)InLocals)

    ,PreviousFrame   (InPreviousFrame)

    ,OutParms (NULL)

 

UStruct * InNode 中的noxss 是字节数组,包含实际被执行的二进制脚本代码。Code表示脚本执行的起始地址(&InNode->noxss(CodeOffset))。脚本通过Step()进行单步执行

FORCEINLINE void FFrame::Step(UObject *Context, RESULT_DECL)

{

      INT B = *Code++;

      (Context->*GNatives[B])(*this,Result);

}

这里解释一下,语句(Context->*GNatives[EX_Let])(*this,Result); 等价于

Context->execLet(*this,Result); 下面看个实际的例子: 下面的脚本代码

 

local x;

x = 5

经过编译后形成如下脚本指令

EX_Let

EX_LocalVariable

4-byte variable ID corresponding to 'x'

EX_IntByteConst

5

ExcLet 函数被调用, 当前指令指针Code  指向Ex_LocalVariable, execLet 主要负责变量分配-即查找变量,存储分配结果。但它不处理 = 操作符号,变量赋值操作,这个操作将由其他的ExecXXX()方法处理,因此该函数在执行后,需要调用 FFrameStep操作继续驱动指令执行。

FFrame 类也有一些重二进制代码中,读取变量的辅助函数: ReadInt(), ReadObject(), ReadFloat(), ReadWord(), and ReadName().

除此之外,引擎中也定义了一些宏,直接对棧操作,并返回 给指定的变量,

#define P_GET_INT(var) INT var=0;

Stack.Step(Stack.Object,&var);

#define P_GET_INT_OPTX(var,def) INT   var=def; Stack.Step(Stack.Object,&var);

 

4.   事件函数

            在引擎中定义了一些关键事件函数,驱动脚本系统的执行,以AActor为例,Engine/Inc/EngineClasses.h  eventTick(), eventDestroyed(), eventBeginPlay(), eventPostBeginPlay(), 应用程序启动后,先调用eventBeginPlay() 在调用eventPostBeginPlay(), 在程序Update时调用eventTick()函数。 这些EventXXX()比较类似,函数实现,大多使用自动生成的AActor_XXX_Parms 作为参数,通过FindFuctionCheck找到脚本函数,最后统一调用ProcessEvent()函数进行下一步的处理。

struct Interaction_eventTick_Parms

{

    FLOAT DeltaTime;

    Interaction_eventTick_Parms(EEventParm)

    {

    }

};

 

void eventTick(FLOAT DeltaTime)

{

    Interaction_eventTick_Parms Parms(EC_EventParm);

    if(IsProbing(NAME_Tick)) {

    Parms.DeltaTime=DeltaTime;

    ProcessEvent(FindFunctionChecked(ENGINE_Tick),&Parms);

    }

}

 

5.   ProcessEvent()

       ProcessEvent()首先检查函数的可执行性 ,然后为函数的执行,创建一个新的棧,启动计时器将函数的参数,拷贝到棧对象的Local成员变量中,最后并将其作为函数的第一个参数传入.

// Call native function or UObject::ProcessInternal.

(this->*Function->Func)(NewStack, (BYTE*)Parms + Function->ReturnValueOffset);

 

若是脚本函数,则会调用 UObject::ProcessInternal(),做进一步处理后,会将UFunctionContructorLink维护的全部UProperty对象,即:脚本函数使用的本地变量,全部删除掉。然后关闭计时器。

void UObject::ProcessEvent( UFunction* Function, void* Parms, void* UnusedResult )

{

    static INT noxssEntryTag = 0;

    // Reject.

    if

    (   (!(Function->FunctionFlags & (FUNC_Native | FUNC_Defined)))

    ||  !IsProbing( Function->GetFName() ) ||  IsPendingKill()

    ||  Function->iNative|| ((Function->FunctionFlags & FUNC_Native) && ProcessRemoteFunction(Function, Parms, NULL )) )

        return;

    DECLARE_CYCLE_COUNTER(STAT_UnrealnoxssTime);

    if(++noxssEntryTag == 1)

    {

        START_CYCLE_COUNTER(STAT_UnrealnoxssTime);

    }

    {

        STAT(FScopednoxssStats noxssStats( Function ));

        // Create a new local execution stack.

        FFrame NewStack( this, Function, 0, appAlloca(Function->PropertiesSize) );

        checkSlow(NewStack.Locals || Function->ParmsSize == 0);

        // initialize the parameter properties

        appMemcpy( NewStack.Locals, Parms, Function->ParmsSize );

        // zero the local property memory

        appMemzero( NewStack.Locals+Function->ParmsSize, Function->PropertiesSize-Function->ParmsSize );

        for ( UProperty* LocalProp = Function->FirstStructWithDefaults; LocalProp != NULL;LocalProp = (UProperty*)LocalProp->Next )

        {

            UStructProperty* StructProp = Cast<UStructProperty>(LocalProp,CLASS_IsAUStructProperty);

            if ( StructProp != NULL )

            {

                StructProp->InitializeValue(NewStack.Locals + StructProp->Offset);

            }

        }

        (this->*Function->Func)(NewStack, (BYTE*)Parms + Function->ReturnValueOffset);

        for (UProperty* P = Function->ConstructorLink; P; P = P->ConstructorLinkNext)

        {

            if (P->Offset >= Function->ParmsSize)

            {

                P->DestroyValue(NewStack.Locals + P->Offset);

            }

            else if (!(P->PropertyFlags & CPF_OutParm))

            {

                appMemcpy((BYTE*)Parms + P->Offset, NewStack.Locals + P->Offset, P->ArrayDim * P->ElementSize);

            }

        }

    }

 

    if(--noxssEntryTag == 0)

    {

        STOP_CYCLE_COUNTER(STAT_UnrealnoxssTime);

    }

 

 

6.   CallFunction()

只要是脚本中的函数:VirtualFunction(),FinalFunction(),GlobalFunction(),DelegateFunction().,都会调用Callfunction,若是Native函数,则直接调用C++的实现,若是脚本函数,则创建一个新的棧;并处理输入输出参数。最后调用ProcessInternal() 进行函数的内部处理,调用结束后,拷贝输出参数,并清空棧。

7.   ProcessInternal()

ProcesInternal 会对编译过的脚本函数进行递归,检查,防止无穷递归 。然后开始做循环,循环每次检查StateCode 成员变量,若是Ex_Return,即函数结束的位置,原理很简单。 中间的过程就是调用Step方法,将当前处理的Object对象做为参数传入其中。循环体中定义的BUGGFER,存放返回结果。 意,如果当前指令是Ex_DebugInfo,那么要多调一次Step.以便于输出调试信息。

void UObject::ProcessInternal( FFrame& Stack, RESULT_DECL )

{

    guardSlow(UObject::ProcessInternal);

    DWORD SingularFlag = ((UFunction*)Stack.Node)->FunctionFlags & FUNC_Singular;

    if  (   !ProcessRemoteFunction( (UFunction*)Stack.Node, Stack.Locals, ULL )

        &&   IsProbing( Stack.Node->GetFName() )

        &&   !(ObjectFlags & SingularFlag) )

    {

        ObjectFlags |= SingularFlag;

        BYTE Buffer[1024];//!!hardcoded size

        appMemzero( Buffer, sizeof(FString) );//!!

#if DO_GUARD

        if( ++Recurse > RECURSE_LIMIT )

        {

            if ( GDebugger && GDebugger->NotifyInfiniteLoop() )

                Recurse = 0;

            else

                Stack.Logf( NAME_Critical, TEXT("Infinite noxss recursion (%i calls) detected"),RECURSE_LIMIT );

        }

#endif

        while( *Stack.Code != EX_Return )

            Stack.Step( Stack.Object, Buffer );

        Stack.Code++;

        Stack.Step( Stack.Object, Result );

        //DEBUGGER: Necessary for the call stack. Grab an optional 'PREVSTACK' debug info.

        if ( *Stack.Code == EX_DebugInfo )

            Stack.Step( Stack.Object, Result );

        ObjectFlags &= ~SingularFlag;

#if DO_GUARD

        --Recurse;

#endif

    }

   

    unguardSlow;

}

 

 

8.   ProcessRemoteInternal()

该函数主要进行,远程函数的资格判定:  Server Client 的判定, RPC连接性检查,replicate条件检查,网络饱和性检查(在网络饱和的情况下,不发送 Reliable 函数), 符合条件则调用InternalProcessRemoteFunction继续执行。 这里既有客户端的代码也有服务器的代码,比如 RPC连接性检查, 通过GetTopPlayerController 取得Player ,在转换为其子类类型UNetConnection , 进行连接判定处理处理。其他的就不在一一叙述。 

9.   InteralProcessRemoteFunction()

这个函数是真正将函数数据发给远程机器 ,其实两边函数都有,差的就是参数和棧。将其作为参数复制到另一边即可。

 

10. 脚本执行实例

class MainClass extends Pawn;

var MyObject hc;

function PostBeginPlay()

{

 HelperFunc1();

}

 

function HelperFunc1()

{

  local int n1;

  local int n2;

  hc = spawn(class'MyObject');   

  n1 = hc.x;

  n2 = hc.GetNumber();

}

 

class HelperClass extends Actor;

var int x;

function int GetNumber()

{

  return 3;

}

 

 

四、        脚本的编译

 

1.编译相关类

 

描述

所在文件

UMakeCommandlet

 Make输入参数,启动编译

Editor/UMakeCommandlet.cpp

UEditorEngine

启动脚本分析和编译处理

Editor/Editor.h, Editor/UnScrCom.cpp

FnoxssWriter

将编译后的脚本字节码序列化

Editor/UnScrCom.h

FnoxssCompiler

执行实际的编译

Editor/UnScrCom.h, Editor/UnScrCom.cpp

FToken

跟编译原理中的Token一个意思

Editor/UnScrCom.h

 

 

2.脚本的编译流程:

 

http://top.oa.com/pictures/201209/1346925974_52.png


【注】本文作者: hoverzhao(赵睿)

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台