如前文所述,实体是构成游戏的基础,不仅仅是最基础的显示单元,也是最基础的逻辑单元。用一个装B的说法,实体的二向性。
ShinyNova内核中最重要的模块ObjectModule负责维护所有的实体。通常我们会在引擎外部创建一个实体并注册到内核作为游戏逻辑的母体,而之后的开发过程将全部封装在母体内部进行,例如:
class CMyGame:public PObject
{
...
}
m_core.RegistNewObject(new CMyGame);
而在母体内部创建新的实体时,标准方法是调用PGL_MSG_CLASS宏,这个宏将自动完成所有的注册初始化工作:
void CMyGame::OnIniData() //实体标准初始化函数,注册到内核时将被调用
{
PObject* ob1,ob2;
PGL_MSG_CLASS(ob1,PObject);
PGL_MSG_CLASS(ob2,PObject);
}
一旦注册到内核,内核将负责维护所有的实体,包括在进程退出时自动对残余实体进行清理。每个注册到内核实体将被分配一个唯一的句柄,你可以调用函数GetHandle()来获取这个值。也可以在实体内部通过PObject* GetObject(OBHANDLE handle)来获取其他任意注册实体的指针。
而为了方便开发者,内核另有一套实体别名机制,你可以为实体注册一个唯一的别名:
void CMyGame::FindMyDog()
{
DogObject* dog; //DogObject是Pobject的派生类
PGL_MSG_CLASS(dog,DogObject);
dog->ReName("可可"); //注册实体名为"可可"
DogObject* ptrMyDog=(DogObject*)GetObject("可可"); //获取指针
}
“每一粒沙中都可以看到世界!”ShinyNova中游戏逻辑可以在任意一个实体中全部展开,但通常我们会把不同的功能模块封装到不同的实体内。这样通过实体逻辑模块的组合,就可以构建出任意复杂的游戏来。例如最常见的标准按钮:CXyStandButton。将多个按钮和一个背景板实体绑定在一起,就形成了一个控制面板。我们可以定义一个新的实体来处理各个按钮之间的逻辑关系:
class CContrlPlane:public PObject
{
public:
CXyStandButton* btn1;
CXyStandButton* btn2;
PlaneObject* plBkg;
....
void OniniData()
{
PGL_MSG_CLASS(btn1,CXyStandButton);
PGL_MSG_CLASS(btn2,CXyStandButton);
PGL_MSG_CLASS(plBkg,PlaneObject);
}
void OnBtn1(){}
void OnBtn2(){}
}
为了处理实体间复杂的互动关系,引擎采用了与Windows类似的消息机制:
typedef struct _tagSNG_MSG
{
OBJECTHANDLE handle;
MSGHANDLE msgID;
WPARAM param1;
LPARAM param2;
}SNG_MSG,*LPSNG_MSG;
只要知道目标实体的句柄,即可通过PostMsg和SendMsg实现异步或同步通讯。每个实体都将在BOOL OnMsg(SNG_MSG& msg)函数中处理自己接受的消息。需要注意的是,ShinyNova中消息投递是一次性的,如果目标实体不存在,该消息将被立即丢弃。同时无论该实体是否处理该消息(msgID指定),消息都将被投递到OnMsg函数中。
作为逻辑单元,PObject的派生类可以是完全虚拟的,即不可见。但实际上任何一个PObject都有其物理层面,由结构:
typedef struct _tagOBJECT{
double x;
double y;
double z;
double angle;
double alpha;
double width;
double height;
DWORD dwFlags;
DWORD dwScreen; //屏幕层级
BOOL bnShow;
RECT clipRect; //裁剪矩形
}OBJECT,*LPOBJECT;
描述。PObject包含了一系列操作函数来实现对实体状态的改变如:
virtual void MoveTo(double x,double y);
virtual void Move(double dx,double dy);
virtual void SetSize(double width,double height);
virtual void SetFloor(double z);
virtual void UpFloor(double dz);
virtual void UpTop();
virtual void Show();
virtual void Hide();
virtual void SetAlpha(double alpha);
virtual void RotateToAngle(double angle);
virtual void Rotate(double angle);
virtual void RotateToPoint(double x,double y,double angle);
virtual void Scale(double dw,double dh);//按比例防缩
virtual void ScaleTo(double width, double height);
virtual void SetClipRect(RECT rc);
一旦状态变化,系统将自动记录并维护实体的当前状态,并处理实体间的相互遮掩关系。假设我们想把一个实体,向右偏移100像素,并半透明到0.7alpha.只需要:
PObjec* ob;
PGL_MSG_CLASS(ob,PObject);
ob->Move(100,0);
ob->SetAlpha(0.5);
ob->Updata(); //刷新函数,将物体状态更新到内核,重绘
想要方块1漂浮在方块2上面?很简单:
PObject* rect1,rect2;
rect1->UpFloor(rect2->GetZ()+a);//a是任意一个正实数
rect1->Updata();
现在离我们的目标是不是有些近了?我们不需要再去关心怎么画实体,而只需要设计它在那里就可以了,从逻辑到实现的细节将由内核自动完成。
但如果游戏中实体非常之多,那么仍然不够方便.比如一个人由头手脚屁股身子5个实体组成,我们想让人移动一段距离,不得不分别修改5个部分的坐标。如果要移动一个英雄连,那么这个工作量可够吓人的。事实上,大部分实体都具有不同程度的关联关系。比如人的手不可能离开身体单独移动,屁股不可能飞到头上去,而连队也许会排成方阵跟随连长前进。这是实体之间物理信息的传递,从一个实体向其他实体扩散(未考虑可能衰减和反馈)。我们把这种固定的传递关系称为绑定,传递方为父体,被传递方为子体。好的,那我们这样来设计连队的方阵移动:
Offiserbject* offiser; //省略构建部分,下同
for(int i=0;i<100;i++)
{
SoldierObject* s; BodyPart* hand,foot;
s->AddChild(hand,OBJECT_LOCK_ALL); //给士兵添加手 ,OBJECT_LOCK_ALL宏代表关联所有信息
s->AddChild(foot,OBJECT_LOCK_ALL); //给士兵添加脚
... //添加其他部分
offiser->AddChild(s,OBJECT_LOCK_ALL); //将士兵与军官绑定
}
offiser->Move(dx,dy); //全队移动dx,dy像素
offiser->Updata();
是不是轻巧了很多?
实体关联绑定是ShinyNova中非常重要的一个概念,有了这个支持,我们才可以放心专注于各个功能模块的内部设计,而不用担心多模块组合时产生的交互影响。另外,不同的实体绑定关系可能有所差异,有些只需要关联图层,有些需要关联位移,有些甚至需要关联消息。采用何种关联方式,由一个标志位决定:
//物体PObject关联性
#define OBJECT_LOCK_DELETE 0x00000001 //关联存在
#define OBJECT_LOCK_ANGLE 0x00000002 //关联角度
#define OBJECT_LOCK_SHOW 0x00000004 //关联显示
#define OBJECT_LOCK_ALPHA 0x00000008 //关联透明度
#define OBJECT_LOCK_SELECT 0x00000010 //关联可触发
#define OBJECT_LOCK_UPDATA 0x00000020 //关联刷新
#define OBJECT_LOCK_SCALE 0x00000040 //关联放缩
#define OBJECT_LOCK_SIZE 0x00000080 //始终等大小
#define OBJECT_LOCK_MEMORY_SHOW 0x00001000 //关联记忆显示,传递Hide时记录Show信息
#define OBJECT_RECORD_SHOW 0x00002000 //标志已记录显示
#define OBJECT_LOCK_CLIPRECT 0x00004000 //裁剪矩形
#define OBJECT_LOCK_FLOOR 0x00008000 //关联图层
#define OBJECT_LOCK_COORDINATE 0x00010000 //关联坐标
#define OBJECT_LOCK_MSG 0x00020000 //接受父体传递来的消息,否则父体的SendMsgToChild将不发送给该子体
#define OBJECT_LOCK_INPUT 0x00040000 //接收和父体相同的消息
#define OBJECT_LOCK_POSITION OBJECT_LOCK_ANGLE|OBJECT_LOCK_FLOOR|OBJECT_LOCK_COORDINATE|OBJECT_LOCK_SIZE
//全关联性
#define OBJECT_LOCK_ALL OBJECT_LOCK_DELETE /
| OBJECT_LOCK_UPDATA /
| OBJECT_LOCK_SCALE/
| OBJECT_LOCK_SIZE/
| OBJECT_LOCK_POSITION/
| OBJECT_LOCK_SHOW /
| OBJECT_LOCK_ALPHA /
| OBJECT_LOCK_CLIPRECT/
| OBJECT_LOCK_MSG
PS:单个模块(控件)内各个实体的图层差异最好小于1,模块之间的图层差异大于1,以防止发生子体交错(约定俗成)。