WIP源代码:

Github

OSC镜像

对象系统以对象为中心,对象系统的最基本设计策略是基于组件的设计。对象系统将尽量避免使用继承方式来拓展游戏对象,恰当的使用Mix-in来来最属性做拓展,单个属性可以适当使用继承。每个游戏对象都是由属性组装起来的。

组件分为两种,c++组件和脚本组件,脚本组件是在脚本中定义的。一般来讲某些脚本组件是 c++组建的封装,这时仅仅是吧 c++组件实例的指针关联到脚本中,所有通信都由此指针链接。

在 c++中当前的主要对象就是 sprite,这个 sprite 在 lua 中对应 GameObject 类,也就是说每一个 lua 中的游戏实例都会对应一个 c++中的 sprite。Sprite 在 c++中会被储存到 scene 中的 Object 层,这个层仅仅会涉及到游戏对象的渲染相关操作,不会涉及到任何游戏逻辑的更新。在 lua 中,GameObject 被保存到一个列表中,每帧都会更新逻辑。也就是说,整个引擎的对象逻辑都是在 lua 中实现的,与 c++没有任何关系,c++部分只负责基础功能的实现。

下图可以说明问题:

介于 Lua 的灵活性,所有的游戏对象都是数据驱动的。一个对象的组成由下图所示:

必要组件:必要组件都是 c++定义的组件,这些组件是几乎是每个类都需要的组件。包括id,name,transform 之类的东西。
自定义组件:自定义组件有一部分是 c++定义的组件,另外一部分是数据驱动的通用组件,这些组件由脚本或者其他外部数据定义。在对象初始化的时候才被填充。
目前采用的数据驱动方案如下:

初始化场景需要的数据:
•    所有的组件类型
•    激活标记
•    组件数量
[Sprite 数据]
•    静态纹理
•    z_order
[Mesh]
•    mesh 尺寸
[transform]
•    世界坐标
•    缩放
•    锚点
•    旋转 [所有的 Lua 组件]
格式如:
[组件名] 数据个数 = n  
[组件初始化数据表]
[数据名 1]
[数据名 2]
:
:
[数据名 n] [数据] 数据名 1 = n1 数据名 2 = n2 ::数据名 n = nn

初始化过程:

读取场景文件 ----> 解析初始化数据 ----> 将初始化好的对象全部加入 Lua
GameObject 队列 ----> 每个对象调用 ComponentInit 函数初始化所有组件
----> 进入主循环

GameObject 中有一个表是专门用来放置“Component name”-“Component”对的,这些 Component 特指那些 Lua 定义的 Components,访问这些 Components 只能通过这个表使用组件名字来访问。一般对一个 go 读写组件的时候形式如下:

function c0:set_component(component_name,component)          
components[component_name] = component
end
--调用组件只能使用这种方案,否则无法判断组件是否存在
function c0:get_component(component_name)
return components[component_name]
end

注意一般不会在 update 中每帧都访问 get_component(),一般是在 init 中取得这个组件,然后在 update 中使用。
Lua 自定义组件每个组件必须实现一个数据初始化回调函数,此函用用于在初始化时候的组件数据初始化。在场景文件中储存了所有需要初始化的 lua 组件的所有数据,这些数据都是“数据名”-“数据”对,初始化的时候为了使得这些数据初始化到正确的位置,必须调用组件的数据初始化回调函数,一般的回调函数形式如下:

function c1:set_data(data_name,data_val)
if data_name == XXX then
xxx = data_val
end
if data_name == XXX1 then
xxx1 = data_val
end
--...
end

注意:上述方案只是暂时替代方案,有违背数据驱动的思想。
一种组件在一个对象中只能有一个,这在为 go 添加组件的时候会检查,在初始化的时候也首先会检查此组件是否存在(对于固有组件直接 c==nil,对于 lua 组件为 components[name]==nil),如果此组件已经存在会发出警告或者报错。但是推荐在添加组件的时候控制。
初始化的时候,固定初始化一个 go,然后对这个对象加上指定的组件即可。
所有的组件(不管是 c++组件还是 lua 组件,实际上都是 Lua 写好的),每一个 Lua 写出来的组件必须实现一个无参数的 new 方法,这个方法用于在初始化的时候创建此组件对象。
 
组件应该实现的方法:
•    new():构造方法,一般会在构造的时候调用,此方法仅用于构造,仅
初始化很小一部分的必要元数据
•    init():初始化一个组件,所有的初始化都在这个函数中执行
•    game_init():游戏逻辑初始化,与 init 不同的是,此初始化仅用于初
始化游戏逻辑,而 init 更多用于系统上的初始化
•    update(dt):更新函数,所有的更新都在这里,dt 是当前帧的时间
•    game_exit():关卡退出时调用,仅仅是游戏逻辑退出
•    exit():关卡卸载时调用,用于系统退出,会清理一些垃圾之类的
bin/engine/script/GameObject.lua 以及 bin/engine/script/Componnents、bin/engine/script/Utilities/SceneLoader.lua 是对这个方案的初步实现。

下面是以前设计的时候瞎写的一份文档,权当参考不对的地方还请高手前辈斧正:

每个对象的属性都是批量更新的,也就是说所有游戏对象的同一个属性将会集中统一到一起更新。不会使用下列风格的更新模:

virtual void Tank::Update(float dt)

{

// Update the state of the tank itself.

MoveTank(dt);

DeflectTurret(dt);

FireIfNecessary();

// Now update low-level engine subsystems on behalf   // of this tank. (NOT a good idea... see below!)   m_pAnimationComponent->Update(dt);   m_pCollisionComponent->Update(dt);   m_pPhysicsComponent->Update(dt);   m_pAudioComponent->Update(dt);   m_pRenderingComponent->draw();

}

while (true)

{

PollJoypad();

float dt = g_gameClock.CalculateDeltaTime();   for (each gameObject)

{

// This hypothetical Update() function updates   // all engine subsystems!   gameObject.Update(dt);

}

g_renderingEngine.SwapBuffers();

}

取而代之,采用批次更新,使用如下风格,一个优点是可以提高缓存一致性:

virtual void Tank::Update(float dt)

{

// Update the state of the tank itself.

MoveTank(dt);

DeflectTurret(dt);

FireIfNecessary();

// Control the properties of my various engine   // subsystem components, but do NOT update   // them here...

if (justExploded)

{

m_pAnimationComponent->PlayAnimation("explode");

}

if (isVisible)

{

m_pCollisionComponent->Activate();

m_pRenderingComponent->Show();

}   else

{

m_pCollisionComponent->Deactivate();

m_pRenderingComponent->Hide();

}

// etc.

}

while (true)

{

PollJoypad();

float dt = g_gameClock.CalculateDeltaTime();   for (each gameObject)

{

gameObject.Update(dt);

}

g_animationEngine.Update(dt);   g_physicsEngine.Simulate(dt);

g_collisionEngine.DetectAndResolveCollisions(dt);   g_audioEngine.Update(dt);

g_renderingEngine.RenderFrameAndSwapBuffers();

}

批次更新即是最基本的更新原则,可以根据具体的情况调节更新顺序。

其他引用:

{

使用Variant数据结构作为消息公共参数:

struct Variant

{

enum Type

{

TYPE_INTEGER,

TYPE_FLOAT,

TYPE_BOOL,

TYPE_STRING_ID,

TYPE_COUNT//类型总数

}

Type m_type;

union

{

int m_asInteger;         float m_asFloat;

bool m_asBool;

unsigned int m_asStringId;

}

}

另外需要关注的是,对象的依赖关系,有必要按照依赖关系更新对象,可以采用树的结构,会有森林出现。

}

对象消息系统备选方案 1

对象之间的消息传递和事件处理采用消息传递模式,把单个时间封装成类,使用消息队列进行职责链方式传递(类似windows消息队列以及MFC逐级消息传递处理机制)。将事件登记到关联的对象里面去。内存分配解决方案见内存设计方案。

每个事件应该是完全可重入的,也就是在同一帧执行n次和执行1次的效果相同。

对象消息系统备选方案2

数据驱动的事件消息传递系统。即是仅考虑游戏对象传递数据流到其他对象,每个对象含有一个或者多个输入/输出端口。这一点可以参考Unreal Engine的可视化编程系统。但是这种方案实行起来需要更多的工作量。也许可以在选择第一种方案的同时,逐步迭代添加方案二。

脚本系统也将加入到对象系统中,目前选定的方案有两个:

1、回调脚本:使用函数在宿主语言和目标语言之间进行相互调用。

2、组件/属性脚本:在基于组件的设计中,允许脚本或者部分脚本创建新的组件或者属性对象。

参考:数据驱动的设计方案一个对象的组成图如下:  游 戏对象类目前的设计是,它具有一个动态数组

①,这个动态数组用于储存所有游戏对象需要的组件的指针。数组的大小被存放在

②一个静态变量中,这个变量使用一 次性初始化在整个程序开始之前就已经根据外部文件(也许是关卡文件或者是资源数据库)初始化好了,或许也在这个时候申请了此数组,但是不会在此处实例化组 件,实例化组件放在明确的 init阶段或者是构造函数阶段(也许构造函数阶段并不安全,所以放在明确的初始化阶段)。组件的实例化根据外部资源或者其他信 息使用工厂模式进行。

必要组件:必要组件都是c++定义的组件,这些组件是几乎是每个类都需要的组件。包括id,name,transform之类的东西。

自定义组件:自定义组件有一部分是c++定义的组件,另外一部分是数据驱动的通用组件,这些组件由脚本或者其他外部数据定义。在对象初始化的时候才被填充。

③是组件接口,提供组件所有需要的方式,以及或许有组件之间的通信接口。

关于游戏对象间通信:目前的设计是游戏对象之间靠一个消息收发器组件通信。

关于游戏对象内部组件之间的通信:

IComponent是所有组件的基类,这个基类为所有组件提供了首发消息的方法。

关于游戏对象的查询:

典型的查询方法是依次调用:

//游戏对象.组件.方法

Object.transform.position();

对于c++类,这些调用不足为惧;但是对于自定义对象这些方法往往都是脚本方法,所以是否可以使用“.”运算符号来调用还需要更多的思考。

关于脚本属性对c++属性的查询:

这个问题有些棘手,目前想到的解决方案是使用脚本(lua)实现类

(class),然后把游戏对象整体传给脚本,然后由脚本调用对象实例的数据。

关于脚本如何获得数据驱动游戏对象的实例,依然是一个问题。

关于脚本属性对脚本属性的查询:

这个可以在脚本内部实现查询。但这是有问题的,因为脚本无法知道查到的属性属于哪一个游戏对象。所以此问题准备归结到上一个问题。

关于数据流的传递接口:

数据流方法用于实现图形化对象逻辑编程,但是此系统颇为复杂,还尚未设计。

⑤类型的对象分为c++定义类和脚本定义类。

脚本定义类还未设计完成,主要包括以下遗留问题:

1、自定义的脚本组件被设计为由数据和函数组成。数据就是一个组件所包含的数据,函数就是一个组件所包含的功能。类似于一个类的组成,数据成员和函数成员。

现在问题是:

脚本数据如何映射到c++类中,使用Variant类型是一种解决方案,用于动态的创建一组属性,但是新问题是,脚本每次更新数据之后如何传回c++类成员,是每帧都交换一次还是仅在调用时交换。

一种正在考虑的解决方案是:

脚 本定义的组件仅提供函数调用。读写一个数据也只能通过getter和setter 来实现。仅仅只在需要的时候才执行那些函数。这样,在初始化的时候需要在 c++类中注册那些所有在脚本中定义的函数。这样脚本函数只需要执行c++操作和返回数据即可。问题是如何在初始化的时候自动的注册那些函数,引擎怎么知 道需要注册哪些函数?注册的函数又怎么储存?前一个问题可以考虑在脚本中植入一个自定义的初始化函数,这个函数用于在初始化c++类的时候提供所有需要注 册的函数的函数名以及参数个数和类型。但是需要详细思考。

临时辅助方案:

初始化场景需要的数据:

  • 对象类型
  • 当前所有c++组件类型
  • 当前所有Lua组件类型
  • 激活标记

[Sprite数据]

  • 世界坐标
  • 静态纹理

[各c++必要组件的初始化数据]

  • Mesh
  • Transform
  • Collider

[各c++非必要组件]

  • Animation
  • AudioSource
  • AudioListener
  • Camera(必须指定主相机,否则无法运行)
  • ParticleEmittter
  • RigidBody
  • ...

[Lua组件]

  • ...

目前采用的方案(优先选择树状结构的文件格式):

初始化场景需要的数据:

  • 所有的组件类型
  • 激活标记
  • 组件数量

[Sprite数据]

  • 静态纹理
  • z_order

[Mesh]

  • mesh尺寸

[transform]

  • 世界坐标
  • 缩放
  • 锚点
  • 旋转 [所有的Lua组件] 格式如:

[组件名] 数据个数 = n  [组件初始化数据表]

[数据名1]

[数据名2]

:

:

[数据名n] [数据] 数据名1 = n1 数据名2 = n2 ::数据名n = nn 初始化过程:

读取场景文件 ----> 解析初始化数据 ----> 将初始化好的对象全部加入Lua GameObject队列 ----> 每个对象调用ComponentInit函数初始化所有组件 ---> 进入主循环

GameObject中有一个表是专门用来放置“Component name”-“Component”对的,这些Component特指那些Lua定义的Components,访问这些Components 只能通过这个表使用组件名字来访问。一般对一个go读写组件的时候形式如下:

function c0:set_component(component_name,component)    components[component_name] = component end

--调用组件只能使用这种方案,否则无法判断组件是否存在

function c0:get_component(component_name)   return components[component_name] end

注意一般不会在update中每帧都访问get_component(),一般是在init中取得这个组件,然后在update中使用。

Lua自定义组件每个组件必须实现一个数据初始化回调函数,此函用用于在初始化时候的组件数据初始化。在场景文件中储存了所有需要初始化的lua组件的所有数据,这些数据都是“数据名”-“数据”对,初始化的时候为了使得这些数据初始化到正确的位置,必须调用组件的数据初始化回调函数,一般的回调函数形式如下:

function c1:set_data(data_name,data_val)    if data_name == XXX then       xxx = data_val

end

if data_name == XXX1 then       xxx1 = data_val    end    --... end

注意:上述方案只是暂时替代方案,有违背数据驱动的思想。

一种组件在一个对象中只能有一个,这在为go添加组件的时候会检查,在初始化的时候也首先会检查此组件是否存在(对于固有组件直接c==nil,对于

lua组件为components[name]==nil),如果此组件已经存在会发出警告或者报错。但是推荐在添加组件的时候控制。初始化的时候,固定初始化一个go,然后对这个对象加上指定的组件即可。

所有的组件(不管是c++组件还是lua组件,实际上都是Lua写好的),每一个 Lua写出来的组件必须实现一个无参数的new方法,这个方法用于在初始化的时候创建此组件对象。

组件应该实现的方法:

  • new():构造方法,一般会在构造的时候调用,此方法仅用于构造,仅初始化很小一部分的必要元数据
  • init():初始化一个组件,所有的初始化都在这个函数中执行
  • game_init():游戏逻辑初始化,与init不同的是,此初始化仅用于初始化游戏逻辑,而init更多用于系统上的初始化
  • update(dt):更新函数,所有的更新都在这里,dt是当前帧的时间
  • game_exit():关卡退出时调用,仅仅是游戏逻辑退出
  • exit():关卡卸载时调用,用于系统退出,会清理一些垃圾之类的

下面是一个参考的读取场景的代码段:

(严重注意:这个代码段中有的变量不属 于组件而是属于GameObject对象,这些对象是无法别对其他组件调用的,因为每一个可调用的数据都必须是一个组件的成员,否则此数据无法被组件访 问,如果需要访问这些组件,必须将这些组件打包到一个单独的组件中,比如打包到go组件中。)

function get_component_port(comp_name)

        s = "local _ = require \""..comp_name.."\";return _:new()"

        return loadstring(s)

end 
function SceneLoader.load_scene(file)
--g_game_objcts
local scene_ptr = app.scene_create()
local go_pak = {}
xml.loadxml(file)
local go_n = SceneLoader.get_go_total()
for i=,go_n do
local ns = tostring(i)
if SceneLoader.get_go_type(i)=="object" then
--n-th object creating local sprite_ptr = SceneLoader.load_component_sprite(ns)
local cmesh = SceneLoader.load_component_mesh(ns)
local ctransform = SceneLoader.load_component_transform(ns,sprite_ptr)
local canimation = SceneLoader.load_component_animation(ns)
local ccollider = SceneLoader.load_component_collider(ns) local go = GameObject:new()
local name = SceneLoader.get_go_name(i) canimation:internal_init(sprite_ptr) go:init(sprite_ptr,name)
go:set_active(SceneLoader.get_go_active(i)) go:add_mesh(cmesh)
go:add_animation(canimation)
ccollider:internal_init(sprite_ptr)
ccollider:setActive(SceneLoader.get_go_collider(ns,"active"))
ccollider:resetType(SceneLoader.get_go_collider(ns,"type")) go:add_collider(ccollider) go:add_transform(ctransform) --load custom components
local names = {}
local cn = SceneLoader.get_go_components(ns,"n")
for i=,cn do
local name = SceneLoader.get_go_components(ns,"e"..tostring(i))
table.insert(names,name)
end
SceneLoader.load_component_custom(ns,go,names) app.scene_add_object(scene_ptr,go.sprite_ptr)
table.insert(go_pak,go) --读取UI对象
elseif SceneLoader.get_go_type(i)=="ui" then local name = SceneLoader.get_wip_node("e"..ns..".name")
local uitype = SceneLoader.get_wip_node("e"..ns..".ui") local x = SceneLoader.get_wip_node("e"..ns..".x")
local y = SceneLoader.get_wip_node("e"..ns..".y")
local w = SceneLoader.get_wip_node("e"..ns..".w")
local h = SceneLoader.get_wip_node("e"..ns..".h") local uiret = nil if uitype=="PictureWidget" then
uiret = SceneLoader.load_picture(x,y,w,h,ns)
elseif uitype=="ButtonWidget" then
uiret = SceneLoader.load_button(x,y,w,h,ns)
elseif uitype=="ScrollerWidget" then
uiret = SceneLoader.load_scroller(x,y,w,h,ns)
end uiret.name = name app.scene_add_ui(scene_ptr,uiret.ptr) UI.addObject(uiret)
end end
local scenepak = {}
scenepak.scene_ptr = scene_ptr
scenepak.objects = go_pak
table.insert(g_running_scenes,scenepak)
end

WIP源代码:

Github

OSC镜像

【2D游戏引擎】那些年对游戏对象的思考的更多相关文章

  1. Love2D游戏引擎制作贪吃蛇游戏

    代码地址如下:http://www.demodashi.com/demo/15051.html Love2D游戏引擎制作贪吃蛇游戏 内附有linux下的makefile,windows下的生成方法请查 ...

  2. 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程03:碰撞检测》

    3.碰撞检测 碰撞检测的概述: 碰撞在物理学中表现为两粒子或物体间极端的相互作用.而在游戏世界中,游戏对象在游戏世界自身并不受物理左右,为了模拟真实世界的效果,需要开发者为其添加属性,以模拟真实事件的 ...

  3. 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程08:虚拟键盘实现》--本系列完结

    8.虚拟键盘实现 概述: 硬键盘就是物理键盘,平时敲的那种.软键盘是虚拟的键盘,不是在键盘上,而是在"屏幕"上.虚拟按键就是虚拟键盘的一部分,根据功能需求,提供部分按键效果的UI可 ...

  4. 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程:简介及目录》(附上完整工程文件)

    介绍:讲述如何使用Genesis-3D来制作一个横版格斗游戏,涉及如何制作连招系统,如何使用包围盒实现碰撞检测,软键盘的制作,场景切换,技能读表,简单怪物AI等等,并为您提供这个框架的全套资源,源码以 ...

  5. 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程07:UI》

    概述: UI即User Interface(用户界面)的简称.UI设计是指对软件的燃机交互.操作逻辑.界面美观的整体设计.好的UI设计不仅可以让游戏变得更有品位,更吸引玩家,还能充分体现开发者对游戏整 ...

  6. 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程06:技能播放的逻辑关系》

    6.技能播放的逻辑关系 技能播放概述: 当完成对技能输入与检测之后,程序就该对输入在缓存器中的按键操作与程序读取的技能表信息进行匹配,根据匹配结果播放相应的连招技能. 技能播放原理: 按键缓存器中内容 ...

  7. 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程05:技能读表》

    5.技能读表 技能读表概述: 技能读表,作为实现技能系统更为快捷的一种方式,被广泛应用到游戏开发中.技能配表,作为桥梁连接着游戏策划者和开发者在技能实现上的关系.在游戏技能开发中,开发者只需要根据策划 ...

  8. 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程04:技能的输入与检测》

    4.技能的输入与检测 概述: 技能系统的用户体验,制约着玩家对整个游戏的体验.游戏角色的技能华丽度,连招的顺利过渡,以及逼真的打击感,都作为一款游戏的卖点吸引着玩家的注意.开发者在开发游戏初期,会根据 ...

  9. 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程02:关键帧动画导入与切割》

    2. 关键帧动画导入与切割 动画的分割与导入概述: 在游戏当中,游戏角色在不同状态下会有不同的动作,这些动作在引擎里相当于一段段的动画片段.当导入模型资源的时候,连同模型动画都会一并导入到引擎中.开发 ...

  10. 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程01: 资源导入》

    1. 资源导入 概述: 制作一款游戏需要用到很多资源,比如:模型.纹理.声音和脚本等.通常都是用其它相关制作资源软件,完成前期资源的收集工作.比如通常用的三维美术资源,会在Max.MAYA等相应软件中 ...

随机推荐

  1. selenium+Python(表单、多窗口切换)

    1.多表单切换 在Web应用中经常会遇到frame/iframe表单嵌套页面的应用,WebDriver只能在一个页面上对元素识别与定位,对于frame/iframe表单内嵌页面上的元素无法直接定位.这 ...

  2. JS支持正则表达式的 String 对象的方法

    注意:本文中所有方法的 RegExp 类型的参数,其实都支持传入 String 类型的参数,JS会直接进行字符串匹配. (相当于用一个简单的非全局正则表达式进行匹配,但字符串并没有转换成 RegExp ...

  3. JS的正则表达式 - RegExp

    RegExp 对象 RegExp 对象表示正则表达式,它是对字符串执行模式匹配的强大工具. 正则表达式的创建方式 1.文字格式,使用方法如下: /pattern/flags  (即:/模式/标记) 2 ...

  4. JVM的内存分配和回收策略

    对象的Class加载 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载.解析和初始化过.如果没有,那必须先执行相应 ...

  5. GIT 恢复单个文件到历史版本

    首先查看该文件的历史版本信息:git log <file> 恢复该文件到某个历史版本:git reset 版本号 <file> 检出改文件到工作区:git checkout - ...

  6. 带有Apache Spark的Lambda架构

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 目标 市场上的许多玩家已经建立了成功的MapReduce工作流程来每天处理以TB计的历史数据.但是谁愿意等待24小时才能获得最新的分析结果? ...

  7. Linux定时增量更新文件--转

    http://my.oschina.net/immk/blog/193926 动机与需求:现在有两台服务器A和B,由于A的存储随时会挂(某些原因),所以需要B机器上有A的备份,并且能够与A同步更新 一 ...

  8. 在Eclipse中生成javadoc

    在<thinking in java>一书的第一章提到javadoc,以前也看过,每次看到这部就跳过了,没有真正去尝试过什么样子,今天终于亲自实践了一下,原来真的挺简单:一.编写java源 ...

  9. H5,API的pushState(),replaceState()和popstate()用法

    pushState和replaceState是H5的API中新添加的两个方法.通过window.history方法来对浏览器历史记录的读写. pushState和replaceState 在 HTML ...

  10. JavaScript中的attachEvent和addEventListener

    attachEvent和addEventListener在前端开发过程中经常性的使用,他们都可以用来绑定脚本事件,取代在html中写obj.onclick=method. 相同点: 它们都是dom对象 ...