User Tools

Site Tools


proton_entity

The Problem

“Update and Render functions? Where we're going we don't need hardcoded Update and Render functions”

First of all, a disclaimer: You don't really *HAVE* to use the Proton Entity System to use the framework. If you already have a system you like, or are porting existing code, you can safely ignore most of this section.

I've made a lot of engines and a lot of game frameworks and for some reason I'm constantly trying new methods, mostly because it keeps things fun and interesting.

Here are some past methods I've tried when creating games:

  • Not knowing what I'm doing and hacking everything (see, Dink source)
    • VERDICT: JUST, NO.
  • Create a giant entity class hierarchy where each kind of thing gets its own class. A bird might be derived like: Object→Visual→MovingObject→NPC→Bird or something ridiculous like that. End up using virtuals everywhere.
    • VERDICT: Slow to add new things, too difficult to refactor, maintenance is a hassle.
  • Create one SUPER HUGE Entity class that does everything, possibly having it initialize sub objects. It ends up with 200 functions and bloated beyond belief. (See Novashell's entity class)
    • VERDICT: Probably the most productive and easy to work with, but becomes a horrible ball of twine eventually where every entity is like a 20KB object when initialized.

Using all of these, this is how it went:

(Ladies, look at your game programming development cycle)

(Now look at mine)

What if we could trade some initial complexity in the beginning for a more sane experience over all?

The Proton Entity System

Aka, “my interpretation of how Unity internals sort of work after I used it for a few hours”.

If you take a look at Entity.h you'll see some functions but nothing about thinking/updating or drawing. So what can an entity do?

Well, not much, alone. It's fairly lightweight at about a 200 byte memory footprint.

The power comes from Components. Components can (but don't have to) render, think, and process input.

They way they accomplish this is by connecting a component function (say, Render) to a function object called “OnRender” (or any name) of their parent, and when that is triggered, the component function (and any others functions connected to it) is called.

There is no looping through the entities to do things, Entities don't even need to be in the same hierarchy or in a list to work. But it's a good idea as the hierarchy makes rendering/thinking order clear.

Test drive

Ok, time to just cannon-ball into the pool and show you some stuff. The important thing to realize is we're just using entities and components to do everything.

If you see a weird function, like, BobEntity(), realize that it's just a helper function from EntityUtils.cpp and is using components to manipulate things to make any entity “bob”. These helpers are not that complicated and just save on repetitive code, so dig in and look at them. Most of them can operate on any kind of entity, without knowing anything about it. If they try to read “pos2d” but the entity doesn't have it set, it will be created with 0,0 instead of crash. If they call a function object that no components have connected to (like, “OnRender” or something) the effect is nothing happens, not an error.

There are a TON of tiny helper functions in EntityUtils.cpp, check the h sometime. My philosophy is if it's a simple one line of code to add a fade, zoom, or bob to a visual, you are that much more likely to do it.

If you'd like to work along with me, open the RTSimpleApp project now!

//Open App.cpp.  Find MainMenuCreate(pGUIEnt) in App::Update(), comment it out and add two lines so you have this:
 
//MainMenuCreate(pGUIEnt);
AddFocusIfNeeded(pGUIEnt); //so it will render, update, and draw.  Really it just adds a few components.
Entity *pEnt = CreateOverlayEntity(pGUIEnt, "logo", "interface/proton.rttex", 0,0);

Instead of the menu as before, you should see the proton logo rendered. (I've set my screen output to iPhone size in main.cpp btw)

Well, we see the logo there. If you looked inside of CreateOverlayEntity, you'd see it creates an Entity, and adds a RenderOverlay component to it and sets some properties. (All of which are stored in either entity's VariantDB (like pos2d, color, scale2d, etc) or the component's VariantDB if it is specific to the component. (properties like fileName, and framex, framey)

Add a few more lines of code:

//MainMenuCreate(pGUIEnt);
AddFocusIfNeeded(pGUIEnt); //so it will render, update, and draw.  Really it just adds a few components.
Entity *pEnt = CreateOverlayEntity(pGUIEnt, "logo", "interface/proton.rttex", 0,0);
//Instead of drawing at 0,0, let's set the pos2d variable to 100,100 so it draws there instead.
pEnt->GetVar("pos2d")->Set(CL_Vec2f(100,100));
 
//let's move to 200,200 1000ms (one second) later.
GetMessageManager()->SetEntityVariable(pEnt, 1000, "pos2d", CL_Vec2f(200,200));
//and, move back to 0,0 a second after that.
GetMessageManager()->SetEntityVariable(pEnt, 2000, "pos2d", CL_Vec2f(0,0));

So now the ball sort of jerks around the screen. Any variable can be “scheduled” to be set in this way. If the entity is destroyed, any pending messages are also destroyed.

Instead of jerkily moving, we can use an InterpolateComponent to smoothly change one variant value to another. This works numbers, vectors, colors, etc.

Instead of adding InterpolateComponent directly, we'll use some helper functions that apply it to pos2d, but just keep in mind we can do this for any variable.

Replace with this code:

//MainMenuCreate(pGUIEnt);
AddFocusIfNeeded(pGUIEnt); //so it will render, update, and draw.  Really it just adds a few components.
Entity *pEnt = CreateOverlayEntity(pGUIEnt, "logo", "interface/proton.rttex", 0,0);
 
 
CL_Vec2f imageSize = pEnt->GetVar("size2d")->GetVector2();
 
FadeInEntity(pEnt, true, 1000);
 
//zoom to bottom right, take 1000 ms to get there, initiate it in 1000 ms.
ZoomToPositionEntityMulti(pEnt, GetScreenSize()-imageSize, 1000, INTERPOLATE_SMOOTHSTEP, 1000);
//zoom back to start
ZoomToPositionEntityMulti(pEnt, CL_Vec2f(0,0), 1000, INTERPOLATE_SMOOTHSTEP, 2000);
//zoom to bottom right again
ZoomToPositionEntityMulti(pEnt, GetScreenSize()-imageSize, 1000, INTERPOLATE_SMOOTHSTEP, 3000);
FadeOutAndKillEntity(pEnt, true, 1000, 4000);

The logo now fades in, smoothly zooms around the screen a bit, then fades out. (and kills the entity completely)

Notice that most things work by time in milliseconds. This allows very exact sequencing and movement of events. Internally, ZoomToPositionEntityMulti is using the MessageManager to schedule things to happen, most helper functions allow you to control the “start time”. In this way, they sort of act like a scripting language.

Let's add a graphic trail effect to our moving logo.

//MainMenuCreate(pGUIEnt);
AddFocusIfNeeded(pGUIEnt); //so it will render, update, and draw.  Really it just adds a few components.
Entity *pEnt = CreateOverlayEntity(pGUIEnt, "logo", "interface/proton.rttex", 0,0);
 
//only this part is new, we're adding a component here
EntityComponent *pComp = pEnt->AddComponent(new TrailRenderComponent);
 
CL_Vec2f imageSize = pEnt->GetVar("size2d")->GetVector2();
FadeInEntity(pEnt, true, 1000);
 
//zoom to bottom right, take 1000 ms to get there, initiate it in 1000 ms.
ZoomToPositionEntityMulti(pEnt, GetScreenSize()-imageSize, 1000, INTERPOLATE_SMOOTHSTEP, 1000);
//zoom back to start
ZoomToPositionEntityMulti(pEnt, CL_Vec2f(0,0), 1000, INTERPOLATE_SMOOTHSTEP, 2000);
//zoom to bottom right again
ZoomToPositionEntityMulti(pEnt, GetScreenSize()-imageSize, 1000, INTERPOLATE_SMOOTHSTEP, 3000);
FadeOutAndKillEntity(pEnt, true, 1000, 4000);

You see the same thing as before but with trail.

By simply adding the TrailRenderComponent we can add a “trail” to any kind of moving visual, including buttons, text rendering, despite not knowing a thing about them.

In fact, this will even work with visuals not invented yet because the method TrailRenderComponent is using is just recording specific variables such as “pos2d”, “scale2d”, “rotation”, connecting itself to the “OnRender” function object of the parent and calling OnRender() multiple times while controlling those variables. It puts them back to normal when done. This is something only possible with a loosely coupled system.

We accepted TrailRender's default properties but what if we wanted a fancier trail?

First, take a look at TrailRenderComponent::OnAdd. This is where components register the properties/functions they will allow the outside world to read and set.

We see:

//shared with the rest of the entity
	m_pPos2d = &GetParent()->GetVar("pos2d")->GetVector2();
	m_pSize2d = &GetParent()->GetVar("size2d")->GetVector2();
	m_pScale2d = &GetParent()->GetShared()->GetVarWithDefault("scale2d", Variant(1.0f, 1.0f))->GetVector2();
	m_pColor = &GetParent()->GetShared()->GetVarWithDefault("color", Variant(MAKE_RGBA(255,255,255,255)))->GetUINT32();
	m_pAlignment = &GetParent()->GetVar("alignment")->GetUINT32();
	m_pColorMod = &GetParent()->GetShared()->GetVarWithDefault("colorMod", Variant(MAKE_RGBA(255,255,255,255)))->GetUINT32();
	m_pAlpha = &GetParent()->GetShared()->GetVarWithDefault("alpha", Variant(1.0f))->GetFloat();
	m_pRotation = &GetParent()->GetVar("rotation")->GetFloat();  //in degrees
	m_pTrailAlpha = &GetParent()->GetVarWithDefault("trailAlpha", 0.5f)->GetFloat();  
	//register ourselves to render if the parent does
	GetParent()->GetFunction("OnRender")->sig_function.connect(1, boost::bind(&TrailRenderComponent::OnRender, this, _1));
 
	//our own variables/settings
	m_pFrames = &GetVarWithDefault("frames", uint32(5))->GetUINT32(); 
	m_pTimeBetweenFramesMS = &GetVarWithDefault("timeBetweenFramesMS", uint32(50))->GetUINT32(); 

The first big chunk setting up pointers to variables that the parent has (or doesn't have). You can see defaults are set in many cases, this kicks in if they don't exist yet.

Look near the bottom, it's not using the GetParent() prefix, it's using variables from its own VariantDB. A uint32 named “frames” with a default of 5, and a “timeBetweenFramesMS” with a default of 50.

(It means every 50 ms it takes a “snapshot” and it only keeps track of the last 5 frames when drawing the trail)

So those are the two properties we can change!

Add this code somewhere after the TrailRenderComponent is added:

pComp->GetVar("frames")->Set(uint32(20));
pComp->GetVar("timeBetweenFramesMS")->Set(uint32(20));

Then compile and you'll get this:

Whee!

Let's say you want the image to be clickable, and when clicked, it changes to a random tint color. To do this we'll need to add a TouchHandlerComponent. These will call function objects on parent called “OnTouchStart”, “OnOverStart”, “OnOverEnd”, “OnTouchEnd”. It's up to us to connect them to a function to do something. (yep, sigslots again)

Here is the new code:

//first add this anywhere above the App::Update(), it will get called when we "touch" the logo
 
void OnEntityTouched(VariantList *pVList)
{
	CL_Vec2f touchPt = pVList->Get(0).GetVector2();
	Entity *pEntTouched = pVList->Get(1).GetEntity();
 
	LogMsg("Touched %s at %s!", pEntTouched->GetName().c_str(), PrintVector2(touchPt).c_str());
 
	//change to a new random tint
	pEntTouched->GetVar("color")->Set(GetBrightColor());
}
 
//Next, add this to your existing init code from the last section
 
//add the change color when touch stuff
pComp = pEnt->AddComponent(new TouchHandlerComponent);
pEnt->GetFunction("OnTouchStart")->sig_function.connect(&OnEntityTouched);

Clicking (tapping once on a touch device) it will now change the color randomly and show something like “Touched logo at 153.00, 120.00!” on each touch.

Well, we came this far, let's make a few changes so ten of these things spawn and randomly move around forever.

First we'll need to add another separate function to wire to, in addition to the one we did. Here they both are:

void OnEntityTouched(VariantList *pVList)
{
	CL_Vec2f touchPt = pVList->Get(0).GetVector2();
	Entity *pEntTouched = pVList->Get(1).GetEntity();
 
	LogMsg("Touched %s at %s!", pEntTouched->GetName().c_str(), PrintVector2(touchPt).c_str());
 
	//change to a new random tint
	pEntTouched->GetVar("color")->Set(GetBrightColor());
}
 
void MoveEntityToRandomPlace(VariantList *pVList)
{
	Entity *pEnt = pVList->Get(0).GetEntity();
 
	CL_Vec2f vPosToMoveTo = CL_Vec2f(Random(GetScreenSizeX()), Random(GetScreenSizeY()));
 
	int timeToTakeForMoveMS = RandomRange(100, 1000);
	ZoomToPositionEntityMulti(pEnt,vPosToMoveTo, timeToTakeForMoveMS, INTERPOLATE_SMOOTHSTEP, 0);
 
	//let's schedule this to run again at the exact point they reach their final destination, we'll use the
	//message manager
	GetMessageManager()->CallEntityFunction(pEnt, timeToTakeForMoveMS, "MoveToRandomPlace", &VariantList(pEnt));
}

And here is the replacement code for the init:

	//MainMenuCreate(pGUIEnt);
		AddFocusIfNeeded(pGUIEnt); //so it will render, update, and draw.  Really it just adds a few components.
 
		int logosToCreate = 10;
 
		while (logosToCreate--)
		{
 
			//start in a random place
			Entity *pEnt = CreateOverlayEntity(pGUIEnt, "logo", "interface/proton.rttex", Random(GetScreenSizeX()),Random(GetScreenSizeY()));
			EntityComponent *pComp = pEnt->AddComponent(new TrailRenderComponent);
			pComp->GetVar("frames")->Set(uint32(20));
			pComp->GetVar("timeBetweenFramesMS")->Set(uint32(20));
			float scale = RandomRangeFloat(0.4f,1.0f);
			pEnt->GetVar("scale2d")->Set(CL_Vec2f(scale,scale)); //make it a random size
			FadeInEntity(pEnt, true, Random(600)+400); //add a random element to the fade in time
 
			//add the change color when touch stuff
			pComp = pEnt->AddComponent(new TouchHandlerComponent);
			pEnt->GetFunction("OnTouchStart")->sig_function.connect(&OnEntityTouched);
 
			//connect/create a function object called "MoveToRandomPlace" in the entity that we can connect a real function to,
			//then use the MessageManager to trigger it when we want
 
			pEnt->GetFunction("MoveToRandomPlace")->sig_function.connect(&MoveEntityToRandomPlace);
 
			//Call it now to get things started.  We pass in the entity pointer so it knows who to operate on
			pEnt->GetFunction("MoveToRandomPlace")->sig_function(&VariantList(pEnt));
		}

And now you get a bundle of crazy logos bouncing around. Tapping them changes color. Hmm, make them explode and you've almost got a crappy game here!

So now you probably can sort of see the concept of “wiring functions to things and calling them later” - we have only barely touched on what is possible!

Well written components respond to property changes at any time - for instance, if you were to change the “fileName” property of the OverlayRenderComponent to a new image to display, it would understand and the touch rectangle and such would all just work. (It connects to the “on changed” signal of the fileName Variant) This technique can be used for cheapo anims.

proton_entity.txt · Last modified: 2012/02/07 09:38 by aki