混合语言游戏编辑器的研究

it2022-06-30  93

    最近做一个游戏编辑器,使用了C++做引擎支持,lua做脚本。在前两者的基础上,制作了一个C#的游戏编辑器。这只是一个具有娱乐精神的学生之作,并不是什么牛X作品。在这只是总结一下目前的问题和解决办法。snapshot1 观看地址:http://v.youku.com/v_show/id_XMzA5OTU2ODAw.html

    首先是C++层引擎,为了快速开发,我用了许多已有的第三方库,比如图形引擎OGRE,UI引擎CEGUI等。引擎的结构极其简单,首先是GameManager类,它负责各个子系统的调度,所有游戏系统由它来控制。所有的游戏子系统继承于GameComponent,

查看该类:

class GameComponent { public: GameComponent(){} virtual ~GameComponent(){} /* *interface */ bool initialize() { return doInitialize(); } void update(float dtime) { doUpdate(dtime); } void destroy() { doDestroy(); } private: virtual bool doInitialize() = 0; virtual void doUpdate(float dtime) = 0; virtual void doDestroy() = 0; };

  它有3个接口,初始化,更新以及销毁。

所有的游戏子系统都需要重写以上3个方法,并通过GameManager::addGameComponent注册到游戏主系统中。

比如:

GameManager* gameMgr = new GameManager(); //-----------------add component to game manager---------------------- //script system should be created first because ui system will use it gameMgr->addGameComponent(new ScriptSystem()); //render component gameMgr->addGameComponent(new GameRenderer(renderHandle));

  

  将需要的子系统插入到GameManager中,然后在游戏循环中,只需要调用GameManager::update即可统一更新各个子系统。插入到GameManager的子系统会自动初始化。

这之中使用的是一个list,把每个子系统插入到list中,不需要时移除并调用自身的destroy()

这种引擎的结构对于实验品来说非常好用,只要引擎的功能足够简单,那么这样的方式用起来还不错。

GameManager不仅管理游戏子系统,还管理游戏的某个状态。游戏中某个时间只能存在一种状态,可能是主菜单状态,也可能是游戏进行时的状态,当然状态中可以包含子状态,目前未考虑到这点。

状态的管理使用了一个基于栈的有限状态机,它足够简单。

//push a new state to game state stack void pushGameState(GameState* newState); //pop the top state from stack void popGameState(); //pop the top state and push a new state to stack void changeGameState(GameState* newState); //get the top state from stack GameState* getTopGameState(); void setTopGameState(GameState* state);

  使用顺序容器vector来管理状态,vector可以方便的当成栈来使用,当然也可以是数组,但数组不可以动态增加容量,所以需求上不太适合。

使用这种结构出现过一个问题,就是设置栈顶的时候,我使用了迭代器来遍历,并将不适合的目标都移除,但这种做法是错误的,因为vector的迭代器移除后会失效(我没有用临时变量保存,其实可以用迭代器来实现)。所以我使用了简单的数组式遍历的方法:

if(!mGameStates.empty() && state != NULL) { for(unsigned int i =mGameStates.size()-1; i >=0; --i) { if(mGameStates[i] == state) { mGameStates[i]->resume(); break; } else { mGameStates[i]->exit(); mGameStates.pop_back(); } } }

  引擎中使用了类似Unity3D的游戏对象/组件的结构。概括来说,游戏中有GameObject类,但它只是一个容器,用来装各种对象元素。基本的元素是Transform,它会创建一个模型并保存位置和旋转参数。这个元素在GameObject创建的时候是内置的。我还实现了一个元素,AudioElement,它保存了声效子系统创建的Sound,使用sound name作为参数,其实它只是借用了声效子系统的功能而已,也类似于一个容器功能。

元素可以通过使用GameObject::addElement加入到对象中,当要获取的时候使用GameObject::element<Type>()获取。这里使用了一个RTTI来动态识别存在的元素:

//get element by type name //type: Audio, RigidBody, etc... template<typename ElementType> inline ElementType* element() const { TypeInfo info(typeid(ElementType)); foreach(ElementList::value_type v, mElements) { if(info == TypeInfo(typeid(*v))) { return dynamic_cast<ElementType*>(v); } } return NULL; }

  脚本元素是通过GameObject::setScriptFile加入的,传入的脚本会被对象创建到一个全局object (使用的是luabind)中,在GameObject的 wake(), update()和destroy()中读取相应的脚本函数:

if(mScriptFile != "") { ScriptSystem::getSingleton().runScript(mScriptFile); lua_State* L = ScriptSystem::getSingleton().getLuaState(); mScriptGlobals = luabind::globals(L); mScriptGlobals["self"] = this; if(mScriptGlobals.is_valid() && mScriptGlobals["Wake"]) { try { luabind::call_function<void>(ScriptSystem::getSingleton().getLuaState(), "Wake"); } catch(luabind::error& e) { luabind::object errmsg(luabind::from_stack(e.state(), -1)); std::stringstream ss; ss<<errmsg; std::string errstr; ss>>errstr; DebugInfo(errstr.c_str()); DebugInfo("\n"); } } }

  

这里读取了一个lua中的Wake函数作为GameObject的执行体。

接下来是C#编辑器的制作。

使用Swig把需要的C++模块生成C#的接口,这样在C#中可以间接调用引擎的一些方法。

我使用的是CMake生成Swig工程。Wrap的过程中需要注意的一点是,不能暴露接口文件不能识别的类型,比如Ogre::RenderWindow之类的,如果你要在C#中使用它,那么势必要把整个RenderWindow给Wrap了,所以必须采用迂回的方式,尽量不增加工作量。因为Vector3和其他数学类型的结构在编辑器和引擎之间需要来回传递,所以我自己写了一个简单的CVec3和CVec4的结构作为C#端的处理,当然也可以wrap Ogre::Vector3之类的。

一个遇到的问题是Swig生成模板的接口时需要具化,并加上具体的命名空间。

还有一个问题没有解决,就是C#传入一个byte[]或者IntPtr给C++处理,在C++中当作void*处理的,处理完把数据拷贝到该处内存中并返回给C#,还请swig高手指教。

使用.Net平台来构建UI界面还是比较快速的,很多问题也可以马上搜索解决。

主要的几个问题:

第一个是 PropertyGrid中下拉属性支持,因为下拉属性是需要动态变化的,所以不能用枚举和固定数组。

解决办法是自己写个TypeConverter:

//StringList Converter public abstract class StringListConverter : StringConverter { public List<string> mValueList = null; public StringListConverter() { mValueList = new List<string>(); GetValueList(); } public abstract void GetValueList(); ...

  要使用的化只需要传递给它动态的string list即可

public class MaterialListItem : StringListConverter { public override void GetValueList() { mValueList = MaterialEditorForm.Instance.MaterialList; } public override TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) { return new StandardValuesCollection(MaterialEditorForm.Instance.MaterialList); } }

  第二个问题是自定义类型展开,基本上使用继承于ExpandableObjectConverter的类作为TypeConverter开始没有问题,但使用的过程中,当PropertyGrid中值变化的时候,对应的属性并没有变化。

解决办法是在类型转换类中返回编辑后的新实例:

public override bool GetCreateInstanceSupported(ITypeDescriptorContext context) { return true; } public override object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues) { return new Vector3((float)propertyValues["X"], (float)propertyValues["Y"], (float)propertyValues["Z"]); }

  第三个问题是编辑器中编辑状态和游戏运行状态互换的操作,开始我使用的方法是:

保存场景到xml地图中,删除场景中所有物体,在c++端读取刚刚生成的xml文件然后加载场景。返回时c++端摧毁所有物体,编辑器中加载xml地图文件。

这种方法太没效率了,而且非常容易出错,因为它一直在创建物体,删除物体。

后来采用的一个方法是:在编辑器端创建的物体只保存transform元素,并不通过对象工厂创建,运行编辑的游戏时,保存当前场景到XML文件,然后把所有物体隐藏,在C++端使用UUID来作为物体识别符创建XML描述的场景,这样保证每个物体的标识都不一样并且不会和编辑器存在的物体冲突。返回编辑器端时调用C++对象工厂清理场景,然后把编辑器的场景显示。这种方法初步实验还不错,可以自由切换编辑状态和游戏运行状态。

目前完成了简陋的版本,但制作的过程中,没有代码优化,所以接下来需要阅读图形学的知识并深入Ogre源码优化,另外没有引擎没有AI逻辑,也没有完整的物理系统支持,只是简陋地把Bullet包装一下。

慢慢来吧,一边学习一边折腾。

    

转载于:https://www.cnblogs.com/zyphon/archive/2011/10/05/multiple_anguage_game_editor.html

相关资源:数据结构—成绩单生成器

最新回复(0)