|
|
I do not in any way say that my design and approach is the 'golden path' to follow if you want to create your own game engine.
If you know a shortcut, please take it.
The Matrix "Morpheus: You take the blue pill - the story ends, you wake up in your bed and believe whatever you want to believe. You take the red pill - you stay in Wonderland and I show you how deep the rabbit-hole goes". Lets take the red pill .. and create our own engine, read on .. The Demo Solution If you look at the source code in the DirectX SDK you will notice that all the cool demos are more or less coded in a few big classes which utilize the DX utility functions. This is a fine approach for demonstrating stuff, but if you have the healthy desire to create you own 3D engine or complex demos, I believe you are in much better shape my creating a solution with a set of projects. The single threaded framework I'm about to present is constructed this way:
The Engine project contains the core elements like camera, scene manager etc. and it also contains what we need to get DirectX up and running. The Game
project is the startup project which holds our main.cpp where everything starts (good spot for a break point). The library is just a collection of
standalone classes like vector3D, math stuff and so on. The ODE facade goes into the Physic project, and all shaders are collected in the Shader project.
I've extended this solution with a level editor project which uses the engine to build a scene in a what-you-see is what-you-get kind of way. But the editor implementation
is a bit buggy at the moment, so I've removed it for now. You may or may not like this approach, please tweak it in any way you like.
The Kernel
Also, the kernel has similar methods for updating of the logic, called tick, and stuff which must be done just before and after rendering called
PreRender and PostRender respectively. For example, a fire/smoke effect will have the position of quads calculated in the tick method and the
actual drawing will go into to rendering method. The idea behind this is to try to separate the logic from the drawing. If your project grows, this is good pratice.
Scene manager
When a model is rendered, we loop through all the light sources which touch the model and invoked the shader assigned to it for each light
source. Typically, a model will have an ambient shader (background light) and omni/spot light which it also touches. The omni shader for
the model can be tweaked in any way you like, it all depends on how you want the model to appear. The most common omni shader is the diffuse.fx
shader which just creates a standard phong look. The code for drawing a model goes like this:
Apply Shader
After the shader is added, we add textures including volume textures if needed. When the actual drawing takes place we apply a shader and set
appropriate constants: world-view-proj matrix, light source location and so on. If another pass is needed we can change shader and apply that one.
Scene maps
In the code above the cube map class will help us create the actual cube map by looping through the 6 surfaces and store the rendering result
in a surface. Since this step requires us to clear the device and invoke the Begin-End scene calls, a good sport for all this is in the post
rendering. If an actor with an environment map is moved, we will in theory have to re-generate the map. Because this step will eat performance
you may not want to do this each frame, well it all depends on the situation.
Settings manager and serialization
Note the += sign when adding objects. A settings manager will do the loading with the help from a few templates. Since you properly want to do your
loading in some other way, I will skip more comments about it here, and just show the way it is used. It is all about filling vectors with data,
here goes:
Singletons
This singleton template is a well known trick, I'm sure you will find it usefull.
If we don't have any distort effects, this step is just skipped and the kernel does the rendering as usual.
Logging
This leads us to the end of the basic framework. There are more classes in the source, but it's my feeling that it
will be clear to you what is going on by just looking at the code. Once again, this framework is not necessary the correct way
of doing things, it all depends on what kind of 3D project your are working on.
If you right-click each .fx file you can set a custom build step, which will be:
This is for shader model 2.0, supported by all hardware today. You may use a much newer shader model. The shader files are stored as .fx files and
compiled into .fxo. Even though the DirectX create effect method (D3DXCreateEffectFromFile) can load .fx files directly, you gain some performance
by using .fxo when rendering. Different shaders all need some global variables, things like the world-view-proj matrix, light position, textures
and so on. Good practice is to collect all those variables in one file and include that on in all shaders. Here goes a snapshot of this header file:
Methods used by many shaders can also go into this include file:
Intellisense do not work well with .fx files, so I recommend that you keep an easy-to-follow naming standard for everything, and follow that
standard always. Shaders can grow quickly, especially if you create different implementation for each shader model.
Physic objects are stored as actors with the physic parameter set to something different than 'none'. Since physic objects sort of belong to the
Game project, I've added a physic task to the Game project, which will update the simulation each frame. First, what we need is a physic manager
and we need it to load our geometry (actors) into the ODE world. In order to do this I've created a world data struct (tWorldData), here goes:
Now we have a WorldData list, and it is time to put it into ODE:
Notice the dGeomTriMeshDataBuildSimple call and the lines below it; this is the place where our geometry gets into ODE. What we need to do now is
to create the physic objects we want to move around. That is, the actors which have the physic parameter set to box, sphere etc. Since all physic
objects have common attributes, this is a good spot for some inheritance. Physic objects inherit from IPEntity and have create and render methods.
Let's look a the create method first:
The create method for sphere and cylinder is almost the same, its all about knowing the dimensions of the object and call appropriate ODE functions.
The drawing of a physic object it is just a matter of drawing the mesh loaded in the constructor and set the correct translation and rotation according
to what ODE dictates. We also want to light up the physic object as it falls into the range of some omni or spot lights. In order to do this, we need
to maintain a list of lights which touches the object as it is moved around and apply that list to the shader when rendering.
When ODE detects that two objects are colliding, we end up in the nearCallBack method, here is a code snippet of this method
(it is quite long):
This callback is triggered by ODE, and we could now in theory say that we have integrated physics in our project. Well this is more or less true.
If you look in the source code you will find that I've added quite a few more classes and functions. It may take some time to understand ODE, and sometimes the
simulations just go crazy because of small parameter adjustments. Still, it is not that complex to use after all. The author of ODE: Russell Smith, has done a tremendous job!
I leave it up to you to explore ODE further. Enjoy!
|
