Jump to content
  • Advertisement
  • entries
  • comments
  • views

Framework Fun

Sign in to follow this  


About time I posted an update here. Been a while.

I have kind of lost a bit of heart in Om, my scripting language, since I realised that circular references lead to memory leaks unless I implement some kind of garbage collection. I've not given up on it - I will indeed try at some point to add GC to Om, but I decided I needed to start a new game project to cheer myself up.

Whenever I start a game, I end up copying loads of files from the previous project into the new one. This is a habit I got into maybe ten years ago, when I discovered that trying to maintain my own libraries across several projects at once was a nightmare. I'd modify the library to do something in the current project and from that point on, only the current project would compile ever again :)

But, I'm 42 years old, I should be mature enough to be able to maintain my own personal library. So I've been working on creating a game framework, strictly for personal use. I've called it [font='courier new']Gx[/font], which stands for, erm, Glorious Xylophones or something.

Setting up a library is a good exercise actually. It's making me really think about and improve upon the shared code that all my game projects end up using. The last couple of days, I've been working on my resource mapping classes which I primarily use for managing graphics resources (but there was no reason to not write the map stuff in a more generic way).

I have a [font='courier new']Gx::GraphicsResource[/font] base class that all of the graphics resources use as an interface. It contains methods like [font='courier new']release()[/font], [font='courier new']reset()[/font], [font='courier new']isDeviceBound()[/font] and so on which allow us to treat all the resources in a uniform manner during a device reset.

Graphics resources in my games end up needing to be stored in such a way that I can iterate over them before and after a device reset. There are also always two types of graphics resource - what I'll tentatively call "global" resources and what we can call "local" resources for the sake of this discussion.

For example, I commonly have vertex declarations defined once on startup for the various vertex types I'm using and these are shared across the code. But an [font='courier new']Entity[/font] may well need to own a vertex buffer of its own, and the buffer's lifetime should match that of the [font='courier new']Entity[/font].

So the resource map supports two types of storage - one by a [font='courier new']std::string[/font] url for the global resources and another by using handles which allows us to RAII-away the resources when the owner of the handle goes bye-bye.

An improvement I've made while porting all this into [font='courier new']Gx[/font] is to make the handles more strongly typed, using templates, so that you cannot accidentally use a handle from one map type in another. Previously the handles were just a loose wrapper around an index into the map but now I've also added some type information.

We start with [font='courier new']Gx::ResourceId[/font]. In my previous projects this was just a [font='courier new']typedef[/font] for [font='courier new']unsigned int[/font] which meant it had no information about what it was pointing at. Now, we typedef [font='courier new']Gx::Index[/font] from [font='courier new']DWORD[/font] (from the Win API) and make [font='courier new']Gx::ResourceId[/font] a template class.

This lives in [font='courier new']GxResourceHandle.h[/font] where we also define the generic [font='courier new']Gx::ResourceHandle[/font] and [font='courier new']Gx::TypedResourceHandle[/font]. The former you cast to type when you access it, the latter binds the type to the handle when it is declared.

#include #include namespace Gx{template class ResourceMap;template class ResourceId{public: ResourceId() : id(invalidIndex) { } bool valid() const { return id != invalidIndex; }private: friend class ResourceMap; explicit ResourceId(Index id) : id(id) { } Index id;};template class ResourceHandle{public: ResourceHandle() : id(invalidIndex), map(nullptr) { } ~ResourceHandle(){ destroyed(id); } template T &value(); template const T &value() const; Gx::Signal destroyed;private: friend class ResourceMap; Index id; ResourceMap *map;};template class TypedResourceHandle{public: TypedResourceHandle() : id(invalidIndex), map(nullptr) { } ~TypedResourceHandle(){ destroyed(id); } T &value(); const T &value() const; Signal destroyed;private: friend class ResourceMap; Index id; ResourceMap *map;};}[font='courier new']Gx::Signal[/font] is part of my signals/slots implementation which I wrote an article for the site about a while back. It uses varadic templates to provide a reasonably efficient system for communication between objects that are ignorant of each other's type.

The actual implementation of the [font='courier new']value()[/font] methods is implemented in [font='courier new']GxResourceMap.h[/font] as they need to access methods of the map but divding the files up this way means I can just use forward-declaration in other headers for classes that own a handle and just include the actual map in the .cpps.

We also have Gx::SharedResourceHandle for consistency. This is a very simple, copyable class that you can use to extract out one of the global resources by url. This just allows us to access a global resource using the same handle interface as the others, and binds its type to the handle when declared to avoid accidentally casting the wrong type.

template class SharedResourceHandle{public: SharedResourceHandle() : id(invalidIndex), map(nullptr) { } T &value(); const T &value() const;private: friend class ResourceMap; Index id; ResourceMap *map;};[font='courier new']GxResourceMap.h[/font] is a larger file, so I'll just present the interface to the map, then look at some usage code.

namespace Gx{template class ResourceMap{public: class iterator { public: bool operator==(const iterator &o) const { return v == o.v && i == o.i; } bool operator!=(const iterator &o) const { return v != o.v || i != o.i; } iterator &operator++(){ i = next(i); return *this; } iterator operator++(int){ Index n = i; i = next(i); return iterator(v, n); } Base &operator*(){ return *((*v)); } Base *operator->(){ return (*v); } private: friend class ResourceMap; iterator(PodVector *v, Index i) : v(v), i(i) { } Index next(Index i){ if(i == v->size()) return i; ++i; while(i < v->size() && !((*v))) ++i; return i; } PodVector *v; Index i; }; class const_iterator { public: bool operator==(const iterator &o) const { return v == o.v && i == o.i; } bool operator!=(const iterator &o) const { return v != o.v || i != o.i; } const_iterator &operator++(){ i = next(i); return *this; } const_iterator operator++(int){ Index n = i; i = next(i); return const_iterator(v, n); } const Base &operator*() const { return *((*v)); } const Base *operator->() const { return (*v); } private: friend class ResourceMap; const_iterator(const PodVector *v, Index i) : v(v), i(i) { } Index next(Index i){ if(i == v->size()) return i; ++i; while(i < v->size() && !((*v))) ++i; return i; } const PodVector *v; Index i; }; ResourceMap(){ } ~ResourceMap(); template T &add(const std::string &url, T *p); template T &add(ResourceHandle &handle, T *p); template T &add(TypedResourceHandle &handle, T *p); template T &get(const ResourceId &id){ return *static_cast(resources[id.id]); } template T &get(const std::string &url){ return get(id(url)); } ResourceId id(const std::string &url) const; Base *operator[](Index id){ return resources[id]; } iterator begin(){ return iterator(&resources, 0); } iterator end(){ return iterator(&resources, resources.size()); } const_iterator begin() const { return iterator(&resources, 0); } const_iterator end() const { return iterator(&resources, resources.size()); }private: Index internalAdd(Base *resource); void handleDestroyed(Index id); Receiver receiver; PodVector resources; PodVector free; std::map mapping;};Nothing particularly earth-shattering in the implementations. But now we have the map methods defined, we can implement the [font='courier new']value()[/font] methods of [font='courier new']Gx::ResourceHandle[/font] and [font='courier new']Gx::TypedResourceHandle:[/font]

template template T &ResourceHandle::value(){ return *(static_cast((*map)[id])); }template template const T &ResourceHandle::value() const { return *(static_cast((*map)[id])); }template T &TypedResourceHandle::value(){ return *(static_cast((*map)[id])); }template const T &TypedResourceHandle::value() const { return *(static_cast((*map)[id])); }template T &SharedResourceHandle::value(){ return *(static_cast((*map)[id])); }template const T &SharedResourceHandle::value() const{ return *(static_cast((*map)[id])); }In a debug mode, we could use [font='courier new']dynamic_cast[/font] checks here to ensure we have the correct type, but this has never actually been an issue when using this type of map for me, so I'm just using [font='courier new']static_cast[/font] now.

The other point of interest regarding the handles is in the [font='courier new']add()[/font] methods.

template template T &ResourceMap::add(const std::string &url, T *p){ Index id = internalAdd(p); mapping[url] = id; return *p;}template template T &ResourceMap::add(ResourceHandle &handle, T *p){ handle.id = internalAdd(p); handle.map = this; receiver.connect(handle.destroyed, this, &ResourceMap::handleDestroyed); return *p;}template template T &ResourceMap::add(TypedResourceHandle &handle, T *p){ handle.id = internalAdd(p); handle.map = this; receiver.connect(handle.destroyed, this, &ResourceMap::handleDestroyed); return *p;}The simplest is the [font='courier new']std::string[/font]-based url add, that simply uses a [font='courier new']std::map[/font] to keep the assocaition.

The handles are non-copyable, but we pass them by reference into the add methods and we have declared the [font='courier new']ResourceMap[/font] as a friend of the handle classes, so it can populate their internals.

The [font='courier new']Gx::ResourceMap[/font] also has a [font='courier new']Gx::Receiver[/font], to which we can connect [font='courier new']Gx::Signals[/font], so we connect the handles [font='courier new']destroyed(Index)[/font] signal up so that when the handles go out of scope, the map is informed and can remove the resources they point to.

template void ResourceMap::handleDestroyed(Index id){ delete resources[id]; resources[id] = 0; free.push_back(id);}We also have a [font='courier new']shared()[/font] method to get back a global, url-based resource so we can use it with a consistent handle interface:

template template SharedResourceHandle ResourceMap::shared(const ResourceId &id){ SharedResourceHandle handle; handle.id = id.id; handle.map = this; return handle;}template template SharedResourceHandle ResourceMap::shared(const std::string &url){ return shared(id(url));}Simple as that. So now we can look at some usage. [font='courier new']Gx::Application[/font] contains a protected [font='courier new']Gx::Graphics[/font] object, which is a composition of a [font='courier new']Gx::GraphicsDeice[/font] and [font='courier new']Gx::ResourceMap[/font]. So lets have examples of url, generic resource and typed resource handles.

class Application : public Gx::Application{public: virtual bool createResources(); virtual void render(float blend); Gx::ResourceHandle vertexBuffer; Gx::TypedResourceHandle vertexShader; Gx::SharedResourceHandle decHandle};bool Application::createResources(){ graphics.resources.add("colorvertexdec", new Gx::VertexDeclaration(/* ... */)).reset(graphics.device); graphics.resources.add(vertexBuffer, new VertexBuffer(/* ... */)).reset(graphics.device); graphics.resources.add(vertexShader, new VertexShader(/* ... */)).reset(graphics.device); decHandle = graphics.resources.shared("colorvertexdec"); return true;}void Application::render(float blend){ graphics.device.setVertexDeclaration(graphics.resources.get("colorvertexdec")); // or auto id = graphics.resources.id("colorvertexdec"); // auto -> Gx::ResourceId graphics.device.setVertexDeclaration(graphics.resources.get(id)); // or graphics.device.setVertexDeclaration(decHandle.value()); Gx::VertexBuffer &buffer = vertexBuffer.value(); // generic handle, cast on access buffer.begin(D3DLOCK_DISCARD); /* ... */ buffer.end(); Gx::VertexShader &shader = vertexShader.value(); // typed handle, type encoded in declaration Gx::Matrix wvp = /* ... */ graphics.device.setVertexShader(shader); graphics.device.vertexShader().setMatrix("worldviewproj", wvp); graphics.device.renderTriangleList(buffer);}The vertex declaration stays in the map until it is manually removed or the map is destroyed, whereas the buffer and shader have their lifetimes controlled by the owning [font='courier new']Application[/font] object.

Internally then in [font='courier new']Gx::Application[/font], we handle device reset and the game loop like this:

int Gx::Application::exec(){ ShowWindow(hw, SW_SHOW); MSG msg; PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE); Timer timer; const float delta = 1.0f / 60.0f; float accumulator = delta; while(msg.message != WM_QUIT) { while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } if(!graphics.device.isLost()) { if(graphics.device.isReadyToReset()) { for(auto &r: graphics.resources) { if(r.isDeviceBound()) { r.release(); } } if(!graphics.device.reset()) { return errorWindow("Unable to reset graphics device"); } for(auto &r: graphics.resources) { if(r.isDeviceBound()) { if(!r.reset(graphics.device)) { return errorWindow("Unable to reset graphics resource"); } } } if(!graphicsDeviceReset()) { return errorWindow("Unable to reset graphics resources"); } } float t = timer.elapsed(Gx::Timer::Option::Restart); accumulator += t; while(accumulator >= delta) { update(delta); accumulator -= delta; } graphics.device.begin(); render(accumulator / delta); graphics.device.end(); } } return 0;}So any resources currently in the map that are device-bound are released and reset at the appropriate time. Otherwise, a standard Gaffer-On-Games. fix-your-timestep game loop calling a couple of virtual methods for the class deriving from [font='courier new']Gx::Application[/font] to fill in.

I'm not actually sure if the generic [font='courier new']Gx::ResourceHandle[/font] is needed since as far as I can remember, I have always known the type that a handle should point at when I declare the handle, but that's the thing about assembling a library - you tend to start thinking in slightly different ways and YAGNI doesn't apply quite as strongly.

[font='courier new']Gx[/font] is currently composed of:

GxCore GxCoreTypes.h GxDataStream.h GxFlags.h GxPodVector.h GxPtrVector.h GxResourceHandle.h GxResourceMap.h GxScopedPtr.h GxSignal.h GxSize.h GxStringFormat.h GxTimer.cpp GxTimer.h GxWindows.cpp GxWindows.hGxGraphics GxCubeMap.h GxDepthStencilSurface.h GxDisplaySettings.h GxFont.h GxGraphics.h GxGraphicsBuffer.h GxGraphicsDevice.h GxGraphicsResource.h GxRenderContext.h GxShader.h GxTexture.h GxVertexDeclaration.h GxVertexElement.hGxMaths GxMathTypess.h GxMatrix.h GxQuaternion.h GxRay.h GxVec2.h GxVec3.hGxApplication GxApplication.hPlan is to add [font='courier new']GxPhysics[/font], a wrapper around Bullet that I already have written but will rewrite as part of the port and [font='courier new']GxAnimation[/font], my own implemetation of a skeleton animation system closely tied to my 3D model editor (Charm).

It is difficult sometimes deciding what should be in the [font='courier new']Gx[/font] library and what is specific to the game. Plan is, once I have the basics of [font='courier new']GxPhysics[/font] working in my test driver application, to start the game project and set up the includes to go directly into the source tree of the [font='courier new']Gx[/font] project, so I can easily change [font='courier new']Gx[/font] while I am working on the game.

Plan is to write a 3D platformer and KEEP IT SIMPLE this time. I always end up trying to write Tomb Raider when I should be trying to write more of a Super Mario 3D style platform game. So I'm going to have a simpler central character - no edge grabbing for now, nice air-controls for cartoon jumping and just try to stay on the straight and narrow and not get over-ambitious.

That is all for now. Thanks for stopping by.

Sign in to follow this  


Recommended Comments

Always enjoy the fact you share so much actual code.

Thank you. Glad someone finds it interesting.

Share this comment

Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!