九、起源引擎序列化和反序列化之元数据类型
在dt_common.h文件里定义了实体属性的几种基本类型:
typedef enum
{
DPT_Int=0,
DPT_Float,
DPT_Vector,
DPT_VectorXY, // Only encodes the XY of a vector, ignores Z
DPT_String,
DPT_Array, // An array of the base types (can't be of datatables).
DPT_DataTable,
#if 0 // We can't ship this since it changes the size of DTVariant to be 20 bytes instead of 16 and that breaks MODs!!!
DPT_Quaternion,
#endif
#ifdef SUPPORTS_INT64
DPT_Int64,
#endif
DPT_NUMSendPropTypes
} SendPropType;
其中DPT_VectorXY、DPT_Quaternion没有使用,DPT_Int64是64位编译模式下用的,DPT_NUMSendPropTypes是用来计算总数的。有效的就是6个类型,下面一一详解。
1.DPT_Int是最基本的int类型,占用4个字节,总共32bit。定义服务端属性的方法是SendPropInt,客户端属性的方法是RecvPropInt。该类型也支持用来定义大小比它小的整型变量如char和short。char占用1字节,short占用2字节。
SendProp SendPropInt(
const char *pVarName,
int offset,
int sizeofVar,
int nBits,
int flags,
SendVarProxyFn varProxy
)
{
SendProp ret;
if ( !varProxy )
{
if ( sizeofVar == 1 )
{
varProxy = SendProxy_Int8ToInt32;
}
else if ( sizeofVar == 2 )
{
varProxy = SendProxy_Int16ToInt32;
}
else if ( sizeofVar == 4 )
{
varProxy = SendProxy_Int32ToInt32;
}
void SendProxy_Int8ToInt32( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID)
{
pOut->m_Int = *((const char*)pData);
}
void SendProxy_Int16ToInt32( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID)
{
pOut->m_Int = *((short*)pData);
}
void SendProxy_Int32ToInt32( const SendProp *pProp, const void *pStruct, const void *pData, DVariant *pOut, int iElement, int objectID)
{
pOut->m_Int = *((int*)pData);
}
在定义属性时里会根据变量的实际大小设置取值时的类型转换方法。如果是char类型,会将取值内存位置当作char去取值,避免取值越界。还有其他几种定义属性的方法是用int实现的,比如SendPropBool:
SendProp SendPropBool(
const char *pVarName,
int offset,
int sizeofVar )
{
Assert( sizeofVar == sizeof( bool ) );
return SendPropInt( pVarName, offset, sizeofVar, 1, SPROP_UNSIGNED );
}
SendPropBool其实就是只使用了1bit传输的int类型。再比如SendPropEHandle
SendProp SendPropEHandle(
const char *pVarName,
int offset,
int sizeofVar,
int flags,
SendVarProxyFn proxyFn )
{
return SendPropInt( pVarName, offset, sizeofVar, NUM_NETWORKED_EHANDLE_BITS, SPROP_UNSIGNED|flags, proxyFn );
}
SendPropEHandle也是用int传输的。这种将属性定义方法再封装可以复用代码,方便维护。
2.DPT_Float是最基本的浮点类型。占用4字节,总共32bit。定义服务端属性的方法是SendPropFloat,客户端属性的方法是RecvPropFloat。
SendProp SendPropFloat(
const char *pVarName,
// Variable name.
int offset, // Offset into container structure.
int sizeofVar,
int nBits, // Number of bits to use when encoding.
int flags,
float fLowValue, // For floating point, low and high values.
float fHighValue, // High value. If HIGH_DEFAULT, it's (1<<nBits).
SendVarProxyFn varProxy
)
{
SendProp ret;
相对于int类型参数多了最小值和最大值的定义,这是因为某些float是用最大值和最小值的差值计算出比例转换成整型传输的。代码如下:
else
{
float fRangeVal = (fVal - pProp->m_fLowValue) * pProp->m_fHighLowMul;
ulVal = RoundFloatToUnsignedLong( fRangeVal );
}
pOut->WriteUBitLong(ulVal, pProp->m_nBits);
编码时将float转换成整型写入网络数据包。
dwInterp = pIn->ReadUBitLong(pProp->m_nBits);
fVal = (float)dwInterp / ((1 << pProp->m_nBits) - 1);
fVal = pProp->m_fLowValue + (pProp->m_fHighValue - pProp->m_fLowValue) * fVal;
return fVal;
解码时从网络数据包读取整型然后用最大值和最小值的差值转成float。单个的角度类型定义方法SendPropAngle也是作为float类型传输的。
3.DPT_Vector是向量类型,实体坐标,角度,速度,大小等内部包含3个维度的float值的属性都可以归到这一类。定义服务端属性的方法是SendPropVector,客户端属性的方法是RecvPropVector。Vector类的本质就是连续的3个float,为了使用方便封装成了新的类。所以它的定义参数和float一样。
SendProp SendPropVector(
const char *pVarName,
int offset,
int sizeofVar,
int nBits, // Number of bits to use when encoding.
int flags,
float fLowValue, // For floating point, low and high values.
float fHighValue, // High value. If HIGH_DEFAULT, it's (1<<nBits).
SendVarProxyFn varProxy
)
{
SendProp ret;
Vector类型在网络传输时编码解码方法如下:
void Vector_Encode( const unsigned char *pStruct, DVariant *pVar, const SendProp *pProp, bf_write *pOut, int objectID )
{
EncodeFloat(pProp, pVar->m_Vector[0], pOut, objectID);
EncodeFloat(pProp, pVar->m_Vector[1], pOut, objectID);
// Don't write out the third component for normals
if ((pProp->GetFlags() & SPROP_NORMAL) == 0)
{
EncodeFloat(pProp, pVar->m_Vector[2], pOut, objectID);
}
else
{
...
}
}
static inline void DecodeVector(SendProp const *pProp, bf_read *pIn, float *v)
{
v[0] = DecodeFloat(pProp, pIn);
v[1] = DecodeFloat(pProp, pIn);
// Don't read in the third component for normals
if ((pProp->GetFlags() & SPROP_NORMAL) == 0)
{
v[2] = DecodeFloat(pProp, pIn);
}
else
{
...
}
}
可以看到传输时是当作三个float变量依次处理的
4.DPT_String是字符串类型。定义服务端属性的方法是SendPropString,客户端属性的方法是RecvPropString。
SendProp SendPropString(
const char *pVarName,
int offset,
int bufferLen,
int flags,
SendVarProxyFn varProxy)
{
SendProp ret;
Assert( bufferLen <= DT_MAX_STRING_BUFFERSIZE ); // You can only have strings with 8-bits worth of length.
ret.m_Type = DPT_String;
ret.m_pVarName = pVarName;
ret.SetOffset( offset );
ret.SetFlags( flags );
ret.SetProxyFn( varProxy );
return ret;
}
string类型在网络传输时编码解码方法如下:
void String_Encode( const unsigned char *pStruct, DVariant *pVar, const SendProp *pProp, bf_write *pOut, int objectID )
{
// First count the string length, then do one WriteBits call.
int len;
for ( len=0; len < DT_MAX_STRING_BUFFERSIZE-1; len++ )
{
if( pVar->m_pString[len] == 0 )
{
break;
}
}
// Optionally write the length here so deltas can be compared faster.
pOut->WriteUBitLong( len, DT_MAX_STRING_BITS );
pOut->WriteBits( pVar->m_pString, len * 8 );
}
void String_Decode(DecodeInfo *pInfo)
{
// Read it in.
int len = pInfo->m_pIn->ReadUBitLong( DT_MAX_STRING_BITS );
char *tempStr = pInfo->m_TempStr;
if ( len >= DT_MAX_STRING_BUFFERSIZE )
{
Warning( "String_Decode( %s ) invalid length (%d)\n", pInfo->m_pRecvProp->GetName(), len );
len = DT_MAX_STRING_BUFFERSIZE - 1;
}
pInfo->m_pIn->ReadBits( tempStr, len*8 );
tempStr[len] = 0;
pInfo->m_Value.m_pString = tempStr;
// Give it to the RecvProxy.
if ( pInfo->m_pRecvProp )
pInfo->m_pRecvProp->GetProxyFn()( pInfo, pInfo->m_pStruct, pInfo->m_pData );
}
编码时首先计算字符串长度,然后先往数据包写入字符串长度,然后写入字符串,解码时先取出字符串长度,然后取相应长度的数据读取到临时字符串。
5.DPT_Array是数组类型。如果需要网络传输的属性是数组,需要用该类型。定义服务端属性的方法是InternalSendPropArray,客户端属性的方法是InternalRecvPropArray。
SendProp InternalSendPropArray(
const int elementCount,
const int elementStride,
const char *pName,
ArrayLengthSendProxyFn arrayLengthFn
)
{
SendProp ret;
这个类型只有数组大小和每个元素大小的信息,是需要配合数组内包含的元素的属性定义方法一起使用的。
#define SendPropArray2( arrayLengthSendProxy, varTemplate, elementCount, elementStride, arrayName ) \
varTemplate, \
InternalSendPropArray( elementCount, elementStride, #arrayName, arrayLengthSendProxy )
这个宏将数组类型包含元素的定义方法放在前面,在之后的序列化元数据处理阶段,引擎会将元素的属性定义填充到m_pArrayProp字段上。然后数组类型才能正常使用。数组类型在网络传输时编码解码方法如下:
void Array_Encode( const unsigned char *pStruct, DVariant *pVar, const SendProp *pProp, bf_write *pOut, int objectID )
{
SendProp *pArrayProp = pProp->GetArrayProp();
AssertMsg( pArrayProp, "Array_Encode: missing m_pArrayProp for SendProp '%s'.", pProp->m_pVarName );
int nElements = Array_GetLength( pStruct, pProp, objectID );
// Write the number of elements.
pOut->WriteUBitLong( nElements, pProp->GetNumArrayLengthBits() );
unsigned char *pCurStructOffset = (unsigned char*)pStruct + pArrayProp->GetOffset();
for ( int iElement=0; iElement < nElements; iElement++ )
{
DVariant var;
// Call the proxy to get the value, then encode.
pArrayProp->GetProxyFn()( pArrayProp, pStruct, pCurStructOffset, &var, iElement, objectID );
g_PropTypeFns[pArrayProp->GetType()].Encode( pStruct, &var, pArrayProp, pOut, objectID );
pCurStructOffset += pProp->GetElementStride();
}
}
void Array_Decode( DecodeInfo *pInfo )
{
SendProp *pArrayProp = pInfo->m_pProp->GetArrayProp();
AssertMsg( pArrayProp, ("Array_Decode: missing m_pArrayProp for a property.") );
// Setup a DecodeInfo that is used to decode each of the child properties.
DecodeInfo subDecodeInfo;
subDecodeInfo.CopyVars( pInfo );
subDecodeInfo.m_pProp = pArrayProp;
int elementStride = 0;
ArrayLengthRecvProxyFn lengthProxy = 0;
if ( pInfo->m_pRecvProp )
{
RecvProp *pArrayRecvProp = pInfo->m_pRecvProp->GetArrayProp();
subDecodeInfo.m_pRecvProp = pArrayRecvProp;
// Note we get the OFFSET from the array element property and the STRIDE from the array itself.
subDecodeInfo.m_pData = (char*)pInfo->m_pData + pArrayRecvProp->GetOffset();
elementStride = pInfo->m_pRecvProp->GetElementStride();
Assert( elementStride != -1 ); // (Make sure it was set..)
lengthProxy = pInfo->m_pRecvProp->GetArrayLengthProxy();
}
int nElements = pInfo->m_pIn->ReadUBitLong( pInfo->m_pProp->GetNumArrayLengthBits() );
if ( lengthProxy )
lengthProxy( pInfo->m_pStruct, pInfo->m_ObjectID, nElements );
for ( subDecodeInfo.m_iElement=0; subDecodeInfo.m_iElement < nElements; subDecodeInfo.m_iElement++ )
{
g_PropTypeFns[pArrayProp->GetType()].Decode( &subDecodeInfo );
subDecodeInfo.m_pData = (char*)subDecodeInfo.m_pData + elementStride;
}
}
数组属性的编码时先计算出数组的大小,然后将数组大小先写入数据包,然后循环处理数组元素,调用元素的编码方法写入数据包。解码时先读取数组大小,然后循环读取数据元素。
6.DPT_DataTable是嵌套的属性定义列表类型,定义服务端属性的方法是SendPropDataTable,客户端属性的方法是RecvPropDataTable。SendPropArray3和RecvPropArray3也是用的这个类型实现的。使用这个的类型的好处是提高代码复用性。游戏逻辑类的父类和子类定义时是用不同的属性列表定义的。子类会将父类的属性定义引入成嵌套的属性定义。这样子类不需要再把父类的每个字段单独定义一次。另外将某些属性封装到一起。可以决定是否将这个属性发送到客户端。
全部评论