2014年5月30日 星期五

自訂UnrealScript內建型別

UnrealScript跟C++一樣可以自訂struct、class、enum型別,但是如果想要增加內建型別的話,還是得修改編譯器才辦得的。雖然新增一個內建型別並不是一件簡單的事情,不過UE的程式結構寫得很系統化,很容易就可以找到要改哪些地方。本篇就來示範一下如何為UnrealScript增加一個倍精確度浮點數型別。

由於UnrealScript裡已經定義了一個名為double的struct在Object.uc如下:
struct {DOUBLE} double
{
    var native const int  A;
    var native const int  B;
};  
所以在本例中的倍精確度浮點數型別就改叫real以避免名稱衝突。當然real寫完之後其實是可以取代原來的double。這個struct只是給原生屬性佔位用的,實際上不能在UnrealScript中當成浮點數使用。

除了加入double型別外,當然還會示範如何加入基本的四則運算:
native(1111) static final operator(20) real +  ( real A, real B );
native(1112) static final operator(20) real -  ( real A, real B );
native(1113) static final operator(16) real *  ( real A, real B );
native(1114) static final operator(16) real /  ( real A, real B );
先在UnNames.h中定義待會會用到的名稱:
REGISTER_NAME( 16, DoubleProperty)
REGISTER_NAME( 17, Real) 
所有的內建型別都有對應的屬性類別,在UnType.h中定義對應double型別的UDoubleProperty類別:
/*-----------------------------------------------------------------------------
 UDoubleProperty.
-----------------------------------------------------------------------------*/

//
// Describes an IEEE 64-bit floating point variable.
//
class UDoubleProperty : public UProperty
{
    DECLARE_CASTED_CLASS_INTRINSIC(UDoubleProperty,UProperty,0,Core,CASTCLASS_UFloatProperty)
    
    // Constructors.
    UDoubleProperty()
    {}
    UDoubleProperty( ECppProperty, INT InOffset, const TCHAR* InCategory, QWORD InFlags )
    : UProperty( EC_CppProperty, InOffset, InCategory, InFlags )
    {}
    
    // UProperty interface.
    void Link( FArchive& Ar, UProperty* Prev );
    UBOOL Identical( const void* A, const void* B, DWORD PortFlags=0 ) const;
    void SerializeItem( FArchive& Ar, void* Value, INT MaxReadBytes, void* Defaults ) const;
    UBOOL NetSerializeItem( FArchive& Ar, UPackageMap* Map, void* Data ) const;
    FString GetCPPType( FString* ExtendedTypeText=NULL, DWORD CPPExportFlags=0 ) const;
    FString GetCPPMacroType( FString& ExtendedTypeText ) const;
    void ExportTextItem( FString& ValueStr, const BYTE* PropertyValue, const BYTE* DefaultValue, UObject* Parent, INT PortFlags ) const;
    const TCHAR* ImportText( const TCHAR* Buffer, BYTE* Data, INT PortFlags, UObject* Parent, FOutputDevice* ErrorText = NULL  ) const;
    virtual void CopySingleValue( void* Dest, void* Src, UObject* SubobjectRoot=NULL, UObject* DestOwnerObject=NULL, struct FObjectInstancingGraph* InstanceGraph=NULL ) const;
    virtual void CopyCompleteValue( void* Dest, void* Src, UObject* SubobjectRoot=NULL, UObject* DestOwnerObject=NULL, struct FObjectInstancingGraph* InstanceGraph=NULL ) const;
    UBOOL HasValue( const BYTE* Data, DWORD PortFlags=0 ) const;
    void ClearValue( BYTE* Data, DWORD PortFlags=0 ) const;
    virtual INT GetMinAlignment() const;
    
    UBOOL GetPropertyValue( BYTE* PropertyValueAddress, UPropertyValue& out_PropertyValue ) const;
    UBOOL SetPropertyValue( BYTE* PropertyValueAddress, const UPropertyValue& PropertyValue ) const;
};
UPropertyValue是一個集合各種UnrealScript內建型別的union,也為它加入DOUBLE型別:
union UPropertyValue
{
...
 DOUBLE    DoubleValue;
...
};
實作如下:
/*-----------------------------------------------------------------------------
 UDoubleProperty.
-----------------------------------------------------------------------------*/

INT UDoubleProperty::GetMinAlignment() const
{
    return sizeof(DOUBLE);
}
void UDoubleProperty::Link( FArchive& Ar, UProperty* Prev )
{
    Super::Link( Ar, Prev );
    ElementSize = sizeof(DOUBLE);
    Offset = Align((GetOuter()->GetClass()->ClassCastFlags & CASTCLASS_UStruct) ? ((UStruct*)GetOuter())->GetPropertiesSize() : 0, GetMinAlignment());
}
void UDoubleProperty::CopySingleValue( void* Dest, void* Src, UObject* SubobjectRoot/*=NULL*/, UObject* DestOwnerObject/*=NULL*/, FObjectInstancingGraph* InstanceGraph/*=NULL*/ ) const
{
    *(DOUBLE*)Dest = *(DOUBLE*)Src;
}
void UDoubleProperty::CopyCompleteValue( void* Dest, void* Src, UObject* SubobjectRoot/*=NULL*/, UObject* DestOwnerObject/*=NULL*/, FObjectInstancingGraph* InstanceGraph/*=NULL*/ ) const
{
    if( ArrayDim==1 )
    {
        *(DOUBLE*)Dest = *(DOUBLE*)Src;
    }
    else
    {
        appMemcpy( Dest, Src, ArrayDim*ElementSize );
    }
}
UBOOL UDoubleProperty::Identical( const void* A, const void* B, DWORD PortFlags ) const
{
    return *(DOUBLE*)A == (B ? *(DOUBLE*)B : 0);
}
void UDoubleProperty::SerializeItem( FArchive& Ar, void* Value, INT MaxReadBytes, void* Defaults ) const
{
    Ar << *(DOUBLE*)Value;
}
UBOOL UDoubleProperty::NetSerializeItem( FArchive& Ar, UPackageMap* Map, void* Data ) const
{
    Ar << *(DOUBLE*)Data;
    return 1;
}
FString UDoubleProperty::GetCPPType( FString* ExtendedTypeText/*=NULL*/, DWORD CPPExportFlags/*=0*/ ) const
{
    return TEXT("DOUBLE");
}
FString UDoubleProperty::GetCPPMacroType( FString& ExtendedTypeText ) const
{
    return TEXT("DOUBLE");
}
void UDoubleProperty::ExportTextItem( FString& ValueStr, const BYTE* PropertyValue, const BYTE* DefaultValue, UObject* Parent, INT PortFlags ) const
{
    ValueStr += FString::Printf( TEXT("%f"), *(DOUBLE*)PropertyValue );
}
const TCHAR* UDoubleProperty::ImportText( const TCHAR* Buffer, BYTE* Data, INT PortFlags, UObject* Parent, FOutputDevice* ErrorText ) const
{
    if ( !ValidateImportFlags(PortFlags,ErrorText) )
        return NULL;
    
    if ( *Buffer == TCHAR('+') || *Buffer == TCHAR('-') || *Buffer == TCHAR('.') || (*Buffer >= TCHAR('0') && *Buffer <= TCHAR('9')) )
    {
        // only import this value if Buffer is numeric
        *(DOUBLE*)Data = appAtod(Buffer);
        while( *Buffer == TCHAR('+') || *Buffer == TCHAR('-') || *Buffer == TCHAR('.') || (*Buffer >= TCHAR('0') && *Buffer <= TCHAR('9')) )
        {
            Buffer++;
        }
        
        if ( *Buffer == TCHAR('f') || *Buffer == TCHAR('F') )
        {
            Buffer++;
        }
    }
    return Buffer;
}
UBOOL UDoubleProperty::HasValue( const BYTE* Data, DWORD PortFlags/*=0*/ ) const
{
    if ( (PortFlags&PPF_LocalizedOnly) != 0 && !IsLocalized() )
    {
        return FALSE;
    }
    return *(DOUBLE*)Data != 0.f;
}

void UDoubleProperty::ClearValue( BYTE* Data, DWORD PortFlags/*=0*/ ) const
{
    // if we only want to clear localized values and this property isn't localized, don't clear the value
    if ( (PortFlags&PPF_LocalizedOnly) != 0 && !IsLocalized() )
    {
        return;
    }
    *(DOUBLE*)Data = 0.f;
}

UBOOL UDoubleProperty::GetPropertyValue( BYTE* PropertyValueAddress, UPropertyValue& out_PropertyValue ) const
{
    UBOOL bResult = FALSE;
    if ( PropertyValueAddress != NULL )
    {
        out_PropertyValue.DoubleValue = *(DOUBLE*)PropertyValueAddress;
        bResult = TRUE;
    }
    return bResult;
}
UBOOL UDoubleProperty::SetPropertyValue( BYTE* PropertyValueAddress, const UPropertyValue& PropertyValue ) const
{
    UBOOL bResult = FALSE;
    if ( PropertyValueAddress != NULL )
    {
        *(DOUBLE*)PropertyValueAddress = PropertyValue.DoubleValue;
        bResult = TRUE;
    }
    return bResult;
}

IMPLEMENT_CLASS(UDoubleProperty);
由於UDoubleProperty是固有類別,需要在CoreClasses.h中手動加入註冊:
#define AUTO_INITIALIZE_REGISTRANTS_CORE \
...
    UDoubleProperty::StaticClass(); \
也需要在SavePackage.cpp中加入核心類別之一:
void InitializeCoreClasses()
{
    // initialize the tracking maps with the core classes
    UClass* CoreClassList[]=
    {
    ...
        UDoubleProperty::StaticClass(),
    ...
    };
除了四則運算外,也需要為double加入對應的常數。另外為了可以在UnrealScript中支援log函式,順便加了轉字串的功能。這些函式需要宣告在UnObjBas.h裡:
class UObject
{
...
    DECLARE_FUNCTION(execDoubleConst);
    DECLARE_FUNCTION(execDoubleToString);
    DECLARE_FUNCTION(execAdd_DoubleDouble);
    DECLARE_FUNCTION(execSubtract_DoubleDouble);
    DECLARE_FUNCTION(execMultiply_DoubleDouble);
    DECLARE_FUNCTION(execDivide_DoubleDouble);
我們需要在UnStack.h為double常數和字串轉換定義對應的列舉值,分別對應到GNatives和GConversions陣列的索引值:
enum EExprToken
{
...
    EX_DoubleConst   = 0x5C,
...
};

enum ECastToken
{
...
    CST_DoubleToString  = 0x61,
...
};

struct FFrame : public FOutputDevice
{ 
...
    DOUBLE ReadDouble();
編譯器在剖析時會給每個內建型別一個整數值作為識別,所以要在UnScript.h的EPropertyType中加入對應double型別的成員:
enum EPropertyType
{
...
    CPT_Double = 15,    
    CPT_MAX    = 16,
};

inline DOUBLE FFrame::ReadDouble()
{
    DOUBLE Result;
#ifdef REQUIRES_ALIGNED_ACCESS
    appMemcpy(&Result, Code, sizeof(DOUBLE));
#else
    Result = *(DOUBLE*)Code;
#endif
    Code += sizeof(DOUBLE);
    return Result;
}

inline VariableSizeType FFrame::ReadVariableSize( UField** ExpressionField/*=NULL*/ )
{
...
    if ( Field != NULL )
... 
    else
    {
    switch ( NullPropertyType )
    {
        case CPT_Double: Result = sizeof(DOUBLE);
            break; 
加入新的指令時,要在ScriptSerialization.h中加入對應的序列化。不過由於op code大於等於EX_ExtendedNative的指令就不用,這邊就只需要處理double常數。
#ifdef SERIALIZEEXPR_INC
...
    else switch( Expr )
    {
...    
 case EX_DoubleConst:
        {
            XFER(DOUBLE);
            break;
        }
... 
編譯器在剖析時會把結果轉存token物件方便後續處理,為了新的double型別,也要加入對應的處理:
class FToken : public FPropertyBase
{
...
    union
    {
... 
        DOUBLE Double;         // If CPT_Double.
    };
    
    FString GetValue()
    {
        if ( TokenType == TOKEN_Const )
        {
            if (IsVector())
...   
        else
        {
            switch ( Type )
            {  
                case CPT_Double:   return FString::Printf(TEXT("%f"), Double);
            }
        }
    }
            
    void AttemptToConvertConstant( const FPropertyBase& NewType )
    {
        check(TokenType==TOKEN_Const);
        switch( NewType.Type )
        {
        case CPT_Double: {DOUBLE     V(0.f);         if( GetConstDouble  (V) ) SetConstDouble  (V); break;}
...
        } 
    } 
    
    void SetConstDouble( DOUBLE InDouble )
    {
        (FPropertyBase&)*this = FPropertyBase(CPT_Double);
        Double   = InDouble;
        TokenType  = TOKEN_Const;
    }
        
    UBOOL GetConstDouble( DOUBLE& R ) const
    {
        if( TokenType==TOKEN_Const && Type==CPT_Double )
        {
            R = Double;
            return 1;
        }
        else if( TokenType==TOKEN_Const && Type==CPT_Float )
        {
            R = Float;
            return 1;
        }
        else if( TokenType==TOKEN_Const && Type==CPT_Int )
        {
            R = Int;
            return 1;
        }
        else if( TokenType==TOKEN_Const && Type==CPT_Byte )
        {
            R = Byte;
            return 1;
        }
        else return 0;
    } 
};
編譯器的主要程式碼放在UnScrCom.cpp裡,因為增加了一個CPT_Double,所以也要對GConversions陣列作對應的調整。為了節省篇幅,在此就只實作字串轉換,其他整數和float對double間的轉換就留給讀者練習。由於CPT_Double是最後一個,所以GConversions最後一列就是對double的轉換,在此都填上CST_Max代表不能轉換。
static EName PropertyTypeToNameMap[CPT_MAX] = 
{
...
    // add new property types here
    NAME_DoubleProperty,
};

static DWORD GConversions[CPT_MAX][CPT_MAX] =
/* String   */ { CST_Max,  CST_ByteToString,   CST_IntToString,    CST_BoolToString, CST_FloatToString,   CST_ObjectToString,CST_NameToString,CST_DelegateToString,CST_InterfaceToString,CST_Max,         CST_Max,         CST_VectorToString,  CST_RotatorToString, CST_Max,            CST_Max,  CST_DoubleToString, },
...
/* Double   */ { CST_Max,  CST_Max,      CST_Max,      CST_Max,    CST_Max,     CST_Max,           CST_Max,         CST_Max,                 CST_Max,         CST_Max,         CST_Max,         CST_Max,             CST_Max,             CST_Max,    CST_Max,  CST_Max, },
};

void FScriptCompiler::EmitConstant( FToken& Token )
{
    check(Token.TokenType==TOKEN_Const);
    
    switch( Token.Type )
    {
        case CPT_Double:
        {
            Writer << EX_DoubleConst;
            Writer << Token.Double;
            break;
        }
...
}
在UnScrCom.cpp裡原本CONVERSION_TRUNCATION的值是104,但為了新增CONVERSION_INT_TO_DOUBLE所以只好加一。這些值表示轉換的成本,成本越小的轉換會被優先採用。為了維持原來的運算結果不變,所以讓int轉成double的成本比float高,這樣如果同時有float和double版本的運算子重載時,int引數會傾向採用原本的float版本。
/*-----------------------------------------------------------------------------
 Type conversion.
-----------------------------------------------------------------------------*/
#define CONVERSION_INT_TO_FLOAT 103
#define CONVERSION_INT_TO_DOUBLE 104
#define CONVERSION_TRUNCATION 105

INT FScriptCompiler::ConversionCost
(
...
    else if( (Source.Type==CPT_Int || Source.Type==CPT_Byte) && Dest.Type==CPT_Double )
    {
        // Conversion to float.
        //AddResultText("ConvertToFloat\r\n");
        return CONVERSION_INT_TO_DOUBLE;
    }
}

UBOOL FScriptCompiler::CompileExpr
(
...
    else
    {
        // save this token in case we need it after recursing further (ex. for dynarray functions below)
        LastToken = Token;
        FScriptLocation AfterLastTokenLocation;
        if( Token.TokenName == NAME_None && RequiredType.Type == CPT_Delegate && Token.TokenType == TOKEN_Const )
...  
        else if
        (( Token.TokenName==NAME_Byte
        || Token.TokenName==NAME_Real
...  
        {
...   
            if( Token.TokenName==NAME_Vector )
...   
            else
            {
                EPropertyType T
                = Token.TokenName==NAME_Byte    ? CPT_Byte
                : Token.TokenName==NAME_Real    ? CPT_Double
...    
}

UBOOL FScriptCompiler::GetVarType
(
...
    if( !GetIdentifier(VarType,1) )
... 
    else if( VarType.Matches(NAME_Real) )
    {
        // Intrinsic Real type
        VarProperty = FPropertyBase(CPT_Double);
    }
... 
}

const TCHAR* GetPropertyTypeText( EPropertyType Type )
{
    switch ( Type )
    {
        CASE_TEXT(CPT_Double);
...
}
在UnScriptMacros.h中有一些方便從UnrealScript堆疊取出變數的巨集,也需要加入對應double的相關巨集:
#define P_GET_DOUBLE(var)                  DOUBLE var=0.f;                                         Stack.Step( Stack.Object, &var    );
#define P_GET_DOUBLE_OPTX(var,def)         DOUBLE var=def;                          INIT_OPTX_EVAL Stack.Step( Stack.Object, &var    );
#define P_GET_DOUBLE_REF(var)              DOUBLE var##T=0.f; GPropAddr=0;                         Stack.Step( Stack.Object, &var##T ); if( GPropObject )GPropObject->NetDirty(GProperty); DOUBLE*   p##var = (DOUBLE  *)GPropAddr; DOUBLE&   var = GPropAddr ? *(DOUBLE  *)GPropAddr:var##T;
#define P_GET_DOUBLE_OPTX_REF(var,def)     DOUBLE var##T=def; GPropAddr=0;          INIT_OPTX_EVAL Stack.Step( Stack.Object, &var##T ); if( GPropObject )GPropObject->NetDirty(GProperty); DOUBLE*   p##var = (DOUBLE  *)GPropAddr; DOUBLE&   var = GPropAddr ? *(DOUBLE  *)GPropAddr:var##T;
最後是四則運算和其他double指令的實作:
///////////////////////////////////
// Double operators and functions //
///////////////////////////////////

void UObject::execAdd_DoubleDouble( FFrame& Stack, RESULT_DECL )
{
    P_GET_DOUBLE(A);
    P_GET_DOUBLE(B);
    P_FINISH;
    
    *(DOUBLE*)Result = A + B;
} 
IMPLEMENT_FUNCTION( UObject, 1111, execAdd_DoubleDouble );

void UObject::execSubtract_DoubleDouble( FFrame& Stack, RESULT_DECL )
{
    P_GET_DOUBLE(A);
    P_GET_DOUBLE(B);
    P_FINISH;
    
    *(DOUBLE*)Result = A - B;
} 
IMPLEMENT_FUNCTION( UObject, 1112, execSubtract_DoubleDouble );

void UObject::execMultiply_DoubleDouble( FFrame& Stack, RESULT_DECL )
{
    P_GET_DOUBLE(A);
    P_GET_DOUBLE(B);
    P_FINISH;
    
    *(DOUBLE*)Result = A * B;
} 
IMPLEMENT_FUNCTION( UObject, 1113, execMultiply_DoubleDouble );

void UObject::execDivide_DoubleDouble( FFrame& Stack, RESULT_DECL )
{
    P_GET_DOUBLE(A);
    P_GET_DOUBLE(B);
    P_FINISH;
    
    if (B == 0.0)
    {
        Stack.Logf(NAME_ScriptWarning,TEXT("Divide by zero"));
    }
    
    *(DOUBLE*)Result = A / B;
} 
IMPLEMENT_FUNCTION( UObject, 1114, execDivide_DoubleDouble );

void UObject::execDoubleConst( FFrame& Stack, RESULT_DECL )
{
    *(DOUBLE*)Result = Stack.ReadDouble();
}
IMPLEMENT_FUNCTION( UObject, EX_DoubleConst, execDoubleConst );

void UObject::execDoubleToString( FFrame& Stack, RESULT_DECL )
{
    P_GET_DOUBLE(D);
    *(FString*)Result = FString::Printf(TEXT("%.6f"),D);
}
IMPLEMENT_CAST_FUNCTION( UObject, CST_DoubleToString, execDoubleToString );
編譯完成後,就可以在UnrealScript中使用double型別了:
exec function TestReal(real A, real B)
{
    `log( A + B );
    `log( A - B );
    `log( A * B );
    `log( A / B );
}
甚至連UnrealScript Debugger現在也都可以支援double型別,在Watch視窗中正確顯示出的數值。

沒有留言:

張貼留言