九、起源引擎序列化和反序列化之元数据类型

608人浏览 / 0人评论

    在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也是用的这个类型实现的。使用这个的类型的好处是提高代码复用性。游戏逻辑类的父类和子类定义时是用不同的属性列表定义的。子类会将父类的属性定义引入成嵌套的属性定义。这样子类不需要再把父类的每个字段单独定义一次。另外将某些属性封装到一起。可以决定是否将这个属性发送到客户端。

 

全部评论