Sign in to follow this  
owl

Kernel aproach for game engine.

Recommended Posts

I've been thinking about the kernel based design proposed in the Enginuity articles. I wonder how many people around here has already implemented such a system for their projects, and I'd also like to know your oppions in pro or against such a design. Thanks in advance. I'm all eyes.

Share this post


Link to post
Share on other sites
Quote:
Original post by I_Smell_Tuna
Uh...isn't that how everyone makes their applications?


I don't see why. But anyway, if do it like that, can you tell me something good or bad about it?

Share this post


Link to post
Share on other sites
Well it's a very managed approach, if something goes wrong you can usually isolate it, and find out exactly what is going wrong in a timely manner, instead of looking through all your code trying to track down a bug.

Share this post


Link to post
Share on other sites
Guest Anonymous Poster
Are you time travellers? That would explain why things from the past appear here, well that's the impression I have when I see people talking about "neat" stuff like that, when in Java we have everything ready today with ZERO effort.

The "services" in the link you posted sound much like what a JVM would do, and for further isolating the components, in terms of independent classloaders and life-cycle management in a microkernel fashion, this would do: http://www.eclipse.org/equinox/.

All those tools are stable and has been available since 20th century. When are you from? 1990?

Share this post


Link to post
Share on other sites
I base my designs off of it.

Granted, I don't use any of the code presented in the tutorials, but I do have a class hierarchy that is similar to that presented in part 2. As mentioned before, modularizing the code makes it easier to spot problems. I would imagine that it would also make it easier to convert to multithreaded code.

Share this post


Link to post
Share on other sites
Guest Anonymous Poster
Quote:
Original post by oggialli
Just loading dll/.so's on demand will do the trick just as well... it's really nothing java specific and NOT related to this design pattern here.


Oh, please! Hahahaha, this one I'll leave you to find the answer by yourself: www.google.com .

Let me check the logic behind it:

- No, I don't want to use the fastest and most modern VM today with the richest tool set and community support. Instead I will:
- Rewrite everything from scratch somewhere else to perform what the VM would but worse;
- Reinvent the wheel with "cool" designs that are available somewhere else already, to do similar things but worse;


Share this post


Link to post
Share on other sites
I know there are some pretty glaring bugs and/or design flaws in the Enginuity articles. The author has said a few times he wished he'd never written the articles... Keep that in mind when you read the articles. There are plenty of clever ideas in there, but there are some silly ones too.

Share this post


Link to post
Share on other sites
Quote:
Original post by Anonymous Poster
Quote:
Original post by oggialli
Just loading dll/.so's on demand will do the trick just as well... it's really nothing java specific and NOT related to this design pattern here.


Oh, please! Hahahaha, this one I'll leave you to find the answer by yourself: www.google.com .

Let me check the logic behind it:

- No, I don't want to use the fastest and most modern VM today with the richest tool set and community support. Instead I will:
- Rewrite everything from scratch somewhere else to perform what the VM would but worse;
- Reinvent the wheel with "cool" designs that are available somewhere else already, to do similar things but worse;
You must be from the future, because what you're describing doesn't sound like the version of Java we have in 2006.

Share this post


Link to post
Share on other sites
I didn't say you shouldn't use Java, Mr. Anonymous Coward. I simply stated Java isn't the only platform offering such functionalities. Moreover, dynamic loading of functionality doesn't have anything to do with the design pattern described here.

As you went for the "your opinion sucks" way without backing it up in any way, I might as well do the same. So I say:

Java sucks big time. The language is a horrible simplification of REAL programming languages like C++. The standard library interfaces are horrible. And of course everyone else should think the same way about Java as I do! Including you, Mr. Anonymous.

See now?

Again, I'm sorry for talking offtopic here. Regarding the design pattern in question, it is a fairly common way of doing things not just in game engines but overall in the software industry.

Share this post


Link to post
Share on other sites
I use a design similar to the one in the Enginuity articles. As has been said already, modular code makes the code cleaner and often easier to isolate bugs.

I use the kernel design, though my implementation is different from that in the articles. I found that I didn't need alot of the other things. For smart pointers and serialization I use Boost, I have a different error logging system, I didn't need the triggers and interpolators or the profiler.

Quote:
Original post by Spoonbender
I know there are some pretty glaring bugs and/or design flaws in the Enginuity articles. The author has said a few times he wished he'd never written the articles... Keep that in mind when you read the articles. There are plenty of clever ideas in there, but there are some silly ones too.


That's interesting. I'm curious what bugs or design flaws you were thinking about?

Share this post


Link to post
Share on other sites
Let's not get teperamental.Personally I love Java simplification over the superdense nature of C++ syntactic sugar.
I don't like operator redefinition,templates,friends, and so on(at least not now).I believe Java has captured the essence of real oop.The problem is it is interpreted + it runs other threads in background-a managed environment-so it tends to be generally slow with a variable speed, depending on background task with no real way to overcome this.
Other than that, it is a wise simplification of C++, more in tone with the magnificent C(C++ is the rococo version of C, while Java is the modernist simplification).
I don't know C#, but I like Java best over C++(though I first learnt C about 12 years ago and Java only about 8).
I don't like javascript (though it may be better suited for its job than a Java implementation) and I really dislike C++(though I make most of my home projects in C++(for speed).

Share this post


Link to post
Share on other sites
Quote:
Original post by James Trotter
That's interesting. I'm curious what bugs or design flaws you were thinking about?


Can't remember exactly. He ranted on about it on the irc channel a while back.
One thing was that the reference counting stuff screwed up when objects were allocated on the stack. And he didn't seem particularly fond of the whole multiple inheritance business either... But as I said, I can't remember, I was just listening to a random rant on IRC a while back. [wink]

Contact the author if you're curious

Share this post


Link to post
Share on other sites
My design is similar to the stuff in the Enginuity articles. Not quite the same, but similar. The articles have got a lot of clever ideas in them, and they're definitely insightful reading for anyone not experienced with OOP, but they aren't the be all and end all of engine design either.

I think it would be a nice starting point to begin on your own engine.

Share this post


Link to post
Share on other sites
I'm using this kernel based approach based on Enginuity, for almost 1,5 year in all my games, and I must say that I'm pretty satisfied with it. Almost all of my engine's subsystems are tasks, also, every game/program/test introduces its own task. I didn't got any major problems with it, most were only little irritating things such as the need to create new files for class which inherits from ITask, implement methods, then create object of that class on heap, check for errors, deallocate it etc.

The one and only big problem I can remember now is that I once stumbled upon one complicated case (I won't describe it in full detail) where custom mouse cursor (some alphablended texture) had to be rendered on top of all other things, but that couldn't be guaranteed by Z-buffer, nor renderer, nor input substem, and so I had to hack it around by creating a little task responsible only for rendering mouse cursor at right time.

However, as I stated above, that was one and only major problem I had with this design. IMHO it's very useful to have sth similar built into engine, since then you're able to add new features into the engine without breaking any dependencies. Ie, it would be very easy for me to add a profiler task for my game, which would be responsible for rendering realtime graphs of how much memory is used, which parts of the engine take how many % of CPU time - it's just a matter of adding one new task... yup, adding one new task to engine which is now 1,5 year old and while I was designing it, I never thought about adding things such as profiler :-) As you see, it's quite modular.

I copied Superpig's code and changed it a bit in some places, and few things have been added (kinda organical growth). Here's my code:

ITask.h:

//! Base class which every other task must implement. It is internally used by Kernel.
//! Should be manually created on heap using operator new() and destroyed at end using delete().

class ITask
{
public:

ITask(const char * _name) : canKill(false), autoDelete(false), priority(0), name(_name) { }

virtual ~ITask() { }

// -------------------------------------------------

//@{

/*

Following three functions must be implemented in every task (although, they can be empty).

*/


//! Called when task is initially added to Kernel list.
virtual bool Start() = 0;

//! Called on every Kernel list update.
virtual void Update() = 0;

//! Called when task is removed from list.
virtual void Stop() = 0;

//@}

// -------------------------------------------------

//@{

//! Called when task was suspended (which means, it stopped getting Update() calls) and now it's going back to work.
virtual void OnResume() { }

//! Called when task is going to be suspended for a while.
virtual void OnSuspend() { }

//@}

// -------------------------------------------------

//@{

//! If you set it to true, this task will be removed from Kernel list.
void SetCanKill(bool _can) { canKill = _can; }

//! Returns whether this task can be removed from Kernel queue.
bool GetCanKill() const { return canKill; }

//@}

// -------------------------------------------------

//@{

//! Used for sorting tasks in the Kernel list.
//! Tasks which have smaller priority, will be updated first.
void SetPriority(ulint _p) { priority = _p; }

//! Returns priority of this task.
ulint GetPriority() const { return priority; }

//@}

// -------------------------------------------------

//! Returns the name of this task.
const char * GetName() const { return name.c_str(); }

bool GetAutoDelete() const { return autoDelete; }

// -------------------------------------------------

protected:

bool canKill, autoDelete;
ulint priority;

std :: string name;

};




Kernel.h:

#include <list>
#include "task.h"

#include "../../Others/Misc/singleton.h"

// -------------------------------------------------

//! Use this macro to get access to Kernel singleton.
#define getKernel SC::Kernel::get()

// -------------------------------------------------

namespace SC
{

// -------------------------------------------------

//! It's the beating heart of the engine, responsible for "pumping" various tasks.
//! There's distinction between tasks that are running, and tasks that are paused.
//! Some functions work on both of them, some only on one kind.
class Kernel : public Singleton<Kernel>
{
public:

Kernel() {}
virtual ~Kernel() {}

// -------------------------------------------------

//! Starts main loop of application.
void Execute();

//! Removes all tasks and closes application.
void CloseApp();

void LogAllTasksInfo() const;

//! Removes all running tasks.
void KillAllTasks();

//! Returns task by its name, that was specified at creating it.
//! Returned task may be on list of running or paused tasks.
//! \return Pointer to task or 0 when task not found.
ITask * GetTaskByName(const char * _name) const;

// -------------------------------------------------

bool AddTask(ITask * _task);

void RemoveTask(ITask * _task);

//! Removes dead tasks from running tasks.
usint RemoveDeadTasks();

// -------------------------------------------------

void SuspendTask(ITask * _task);
void ResumeTask(ITask * _task);

// -------------------------------------------------

private:

typedef std :: list < ITask *> TaskList;
typedef TaskList :: iterator TaskListItor;
typedef TaskList :: const_iterator TaskListConstItor;

TaskList runningTasks;
TaskList pausedTasks;

};

// -------------------------------------------------

}




Kernel.cpp:


#include <algorithm>
//#include <string>
#include "SDL.h"

#include "kernel.h"

#include "../Input/inputUtility.h"
#include "../../Others/Misc/misc.h"
#include "../../Others/Logger/logger.h"

#include "../../Layer1/MemoryMgr/mmgr.h"

// ------------------------------------------------- start Execute

void SC :: Kernel :: Execute()
{

ITask * tmp;
TaskListConstItor itor;
TaskListItor thisIt, itorTmp; // used for erasing
SDL_Event event;

log2("Kernel", "Starting application part.")

// ------------------------------------------------- Deal with SDL events

while (!runningTasks.empty())
{
if ( SDL_PollEvent ( &event ) )
{
if ((event.type == SDL_KEYUP) || (event.type == SDL_KEYDOWN ) || (event.type == SDL_MOUSEBUTTONDOWN) )
{
// theese three must be catched by input subsystem
SDL_PushEvent(&event);
}
else if ( event.type == SDL_QUIT )
{
log2("Kernel", "Came external quit message.")
CloseApp();
}
else if ( event.type == SDL_VIDEORESIZE )
{
log2("Kernel", "Came external window resize message. Ignored.")
}
}

// -------------------------------------------------

// you can close immediately application by pressing F12 in debug builds
#ifdef __SC_DEBUG__

if (getInput.GetKeyboardKeyNowPressed(SDLK_F12))
{
log2("Kernel", "F12 was pressed in debug build - closing program.")
CloseApp();
}

#endif

// -------------------------------------------------

{
// loop and update all active tasks
for (itor = runningTasks.begin(); itor != runningTasks.end(); )
{
tmp = (*itor); // ???????? what's this construction needed for?
++itor;
if (!tmp->GetCanKill()) tmp->Update();
}

// -------------------------------------------------

//loop again to remove dead tasks
for (itorTmp = runningTasks.begin(); itorTmp != runningTasks.end(); )
{
tmp = (*itorTmp);
thisIt = itorTmp;
++itorTmp;

if (tmp->GetCanKill())
{
tmp->Stop();
runningTasks.erase(thisIt);

if (tmp->GetAutoDelete())
delete tmp;

tmp = 0;
}
}

// -------------------------------------------------

}
}
}

// ------------------------------------------------- end Execute

// ------------------------------------------------- start CloseApp

void SC :: Kernel :: CloseApp()
{
KillAllTasks();
// + do anything else
}

// ------------------------------------------------- end CloseApp

// ------------------------------------------------- start AddTask

bool SC :: Kernel :: AddTask(SC :: ITask * _task)
{

if (!_task->Start())
{
logError2("Kernel", "This task couldn't start right now:")
logError2( IntToString(_task->GetPriority()).c_str(), _task->GetName());
return false;
}

TaskListItor it;

ITask* tmp;

for ( it = runningTasks.begin(); it != runningTasks.end(); ++it)
{
tmp = (*it);
if (tmp->GetPriority() > _task->GetPriority()) break;
}

runningTasks.insert(it,_task);

std :: string temp = "Added task: ";
temp += _task->GetName();
temp += ", with priority: ";
temp += IntToString(_task->GetPriority());

log2("Kernel", temp.c_str());

return true;
}

// ------------------------------------------------- end AddTask

// ------------------------------------------------- start RemoveTask

void SC :: Kernel :: RemoveTask(SC :: ITask* _task)
{

if ( std :: find(runningTasks.begin(), runningTasks.end(), _task) != runningTasks.end())
{
_task->SetCanKill(true);
return;
}

logError2("Kernel.RemoveTask", "Specifed task not found.")
}

// ------------------------------------------------- end RemoveTask

// ------------------------------------------------- start RemoveDeadTasks

SC :: usint SC :: Kernel :: RemoveDeadTasks()
{
ITask * tmp;
TaskListItor thisIt, itorTmp;
usint count = 0;

for ( itorTmp = runningTasks.begin(); itorTmp != runningTasks.end(); )
{
tmp = (*itorTmp);
thisIt = itorTmp;
++itorTmp;

if (tmp->GetCanKill())
{
tmp->Stop();
runningTasks.erase(thisIt);
tmp = 0;
++count;
}
}
return count;
}

// ------------------------------------------------- end RemoveDeadTasks

// ------------------------------------------------- start SuspendTask

void SC :: Kernel :: SuspendTask(SC :: ITask* _task)
{
TaskListItor it = std :: find(runningTasks.begin(),runningTasks.end(),_task);

if (it != runningTasks.end())
{
_task->OnSuspend();
runningTasks.erase(it);
pausedTasks.push_back(_task);
return;
}

logError2("Kernel.SuspendTask", "Specifed task not found.")

}

// ------------------------------------------------- end SuspendTask

// ------------------------------------------------- start ResumeTask

void SC :: Kernel :: ResumeTask(SC :: ITask* _task)
{

TaskListItor it = std :: find(pausedTasks.begin(),pausedTasks.end(),_task);

if (it != pausedTasks.end())
{
_task->OnResume();
pausedTasks.erase(it); // faster than remove

ITask* tmp;

for ( it = runningTasks.begin(); it != runningTasks.end(); ++it)
{
tmp = (*it);
if (tmp->GetPriority() > _task->GetPriority()) break;
}

runningTasks.insert(it,_task);
return;
}
logError2("Kernel.ResumeTask", "Specifed task not found.")

}

// ------------------------------------------------- end ResumeTask

// ------------------------------------------------- start GetTaskByName

SC :: ITask * SC :: Kernel :: GetTaskByName(const char * _name) const
{
TaskListConstItor itor;

for ( itor = runningTasks.begin(); itor != runningTasks.end(); ++itor)
{
if (!strcmp((*itor)->GetName(), _name) )
return (*itor);
}

for ( itor = pausedTasks.begin(); itor != pausedTasks.end(); ++itor)
{
if (!strcmp((*itor)->GetName(), _name) )
return (*itor);
}

return 0;

}

// ------------------------------------------------- end GetTaskByName

// ------------------------------------------------- start KillAllTasks

void SC :: Kernel :: KillAllTasks()
{
TaskListItor itor;

for ( itor = runningTasks.begin(); itor != runningTasks.end(); ++itor)
{
(*itor)->SetCanKill(true);
}
log2("Kernel", "Removed all tasks from the list.")

}

// ------------------------------------------------- end KillAllTasks

// ------------------------------------------------- start LogAllTasksInfo

void SC :: Kernel :: LogAllTasksInfo() const
{
TaskListConstItor itor;

log2("Kernel", "Current list of tasks");

beginG("RUNNING")

for ( itor = runningTasks.begin(); itor != runningTasks.end(); ++itor)
{
log2( IntToString((*itor)->GetPriority()).c_str(), (*itor)->GetName());
}

endG;

// -------------------------------------------------

beginG("PAUSED")

for ( itor = pausedTasks.begin(); itor != pausedTasks.end(); ++itor)
{
log2( IntToString((*itor)->GetPriority()).c_str(), (*itor)->GetName());
}

endG;

}

// ------------------------------------------------- end LogAllTasksInfo


Share this post


Link to post
Share on other sites
Quote:
Original post by Koshmaar
Almost all of my engine's subsystems are tasks, also, every game/program/test introduces its own task.

...since then you're able to add new features into the engine without breaking any dependencies. Ie, it would be very easy for me to add a profiler task for my game, which would be responsible for rendering realtime graphs of how much memory is used, which parts of the engine take how many % of CPU time - it's just a matter of adding one new task... yup


That's the major benefit I saw from using this aproach.

What I see as problematic is the interrelation between subsystems. Say, between the world objects, the physics, the renderer etc.

I'm not sure where to place the access point for the subsystems to be able to communicate with each other in this kind of task-fashioned design. I also feel this design pushes me to make heavy use of the singleton pattern, and I feel really tempted to make each subsystem a singleton, which would make the whole tasked stuff more a waste of time inside the loop than a gain.

Share this post


Link to post
Share on other sites
Yes, you're right, it's a big problem... but aren't relations between various subsystems, hard to design, regardless of what is the overall system architecture, hmmm? :-)

In my case, many of my subsystems are singletons, that take advantage of tasks to update their state. Ie, I have a Renderer class, which is a singleton with method Render(), but that method is called by special task called RendererUpdate. I know that many people don't like singletons... and after 1,5 years of working with them, I must say that IMHO over-using singletons is bad (which I've done). In my engine's next iteration, I'm going to introduce as few singletons as possible. Probably not even one. Now it is singletonized by a great factor :-)

...

However, there's at least one way to get rid of them even in this version. As you may see, in Kernel there's a method GetTaskByName, which (theoretically) could be used to remove the need to "singletonize" everything. Simply, make each subsystem a task, and if one your class needs to access some subystem, call Kernel::GetTaskByName("some_subsystem") to obtain pointer to ITask, cast and use it... Btw, yeah, I know that it would be (very) irritating to do that in big application :-)

Another solution is the one preseneted in one of the first chapters of Game Programming Gems #3: have a struct called ISubsystems, which looks like this:


struct ISubsystems
{
IRenderer * renderer;
IAudioMgr * audioMgr;
InputMgr * inputMgr;
//...
}



This structure could be accessed through ie. Kernel. That way you could also gain the benefits of interfaces... but this method its own flaws.

--------


For me, the biggest reason for using this task based approach is the simplicity of main application. There's one short "framework" that is duplicated nearly in all programs. First, there goes the typical engine initialization, what creates all needed subsystems and tasks. Then you call Kernel::Execute() - all is done inside it, by tasks. See:



#include "SC.h"

using namespace SC;

int main(int argc, char ** argv)
{
SculptureClay engine;

engine.InitLib();

getRenderer.SetColorDepth(32);
getRenderer.SetFullscreen(0);
getRenderer.SetScreenSize(800, 600);

getRenderer.EnterGraphicsMode();

engine.InitSubsystems();

getKernel.Execute(); // <--- !!!

getRenderer.CloseGraphicsMode();
engine.ShutdownLib();
return 0;
}



That's it, it shows empty black window, which stays until user clicks X. Now, you may say that your framework/engine also allows you to do the same (and probably even more) using the same number of lines of code (and probably even less). Well, the beaty which hides behind it, is that this code won't change despite whether you're just rendering one pixel, or whether you're programming complex game. Just add one task reponsible for updating game state, and be done with it. I need not to add to engine and tell manually Renderer, AudioMgr, InputMgr, GUI, ParticleMgr, Timer etc. that they should update themselves in specific order each frame, and at the end, release all resources etc. in any of my programs - it's done automatically. Isn't that great? ;-)

Share this post


Link to post
Share on other sites
One way I tought off to avoid making subsystems a singleton would be to have them instanced in an object (call it CEngine). Inside this object you have also the kernel which will update them. Then, since everything in the game is based upon the engine, you may have the chance to access the subsystems from there when appropiate. Wheter to make CEngine a singleton is more a matter of taste than anything else.

Share this post


Link to post
Share on other sites
Would it be more beneficial to time stamp instead of just setting priorities? This is probably only useful in a multithreaded example, but lets say you want the render to happen with a priority of 100. Would it be wise to add a time stamp to the priority and sort this way

ex. at the end of the Update() function for a perticular task do:

if( dontKill )
{
CKernel::getPtr()->AddTask( this, (CurrentTime + myPriority) );
}

and as each task is executed it is removed from the priority list.


This can allow some Tasks to run multiple times (not sure what would need to do this) while others only run once. It could also be used to allow certain Tasks to happen at various times with careful implimentation, say a network Timeout after 30 seconds could be added to the Task List when connecting to a server (ok ok...that's a really bad example, but you get what I am aiming at, right?). Is this a useful/good idea, or has it already been tried and failed/successful?

Share this post


Link to post
Share on other sites
In my implementation, the priority is the amount of milliseconds between each update. I also wrote a timed task wich is run for n milliseconds and then it removes itself from the kernel.

To simplify things, the order of execution could be just the order in which you inserted the tasks. Or there could be groups of tasks, which could be executed according to it's group order.

Share this post


Link to post
Share on other sites
The engine im working on is based on a couple of enginuity articles (the kernel, task and singleton classes) but now its barely recognisable, and im sure to adapt it a bug fix it alot more over the next few months.

For me the enginuity series really helped me out, i kept getting so far in my engine design and it was so over complicated, it didnt even occur to me to use a process style approach before, now i dont use anything else.

Luke.

Share this post


Link to post
Share on other sites

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

Sign in to follow this