User Tools

Site Tools


proton_entity

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
proton_entity [2010/10/25 05:40] sethproton_entity [2012/02/07 09:38] (current) aki
Line 9: Line 9:
 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. 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 past methods I've tried when creating games:+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. +  * Not knowing what I'm doing and hacking everything (see, Dink source) 
-  * 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. +    * **VERDICT:** JUST, NO. 
-  * 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 to work with where each entity is like a 20KB object when initialized.+  * 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: Using all of these, this is how it went:
  
-(Ladies, look at your game development cycle)+(Ladies, look at your game programming development cycle)
  
 {{:complexity_graph1.png?|}} {{:complexity_graph1.png?|}}
Line 29: Line 32:
 ==== The Proton Entity System ==== ==== The Proton Entity System ====
  
-Aka, my version of "my interpretation of how Unity internals sort of work after I used it for a few hours".+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? 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 200 bytes.+Well, not much, alone.  It's fairly lightweight at about 200 byte memory footprint.
  
-** Instantiate an entity **+The power comes from Components.  Components can (but don't have to) render, think, and process input.
  
-Let's do some real code to  give you an idea of how to use these thingsfeel free to type along. (you could write it in App::Update() from the RTSimpleApp example) +They way they accomplish this is by connecting a component function (sayRender) to a function object called "OnRender" (or any nameof their parent, and when that is triggered, the component function (and any others functions connected to it) is called.
-<code cpp> +
-Entity ent; //instantiate it+
  
-//Oknow  we've got one.  We can name it if want. +There is no looping through the entities to do thingsEntities 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.
-ent.SetName("DeLorean");+
  
-LogMsg("The entity is called %s", ent.GetName().c_str());+====Test drive=====
  
-//Shortcut: we could've just done Entity ent("DoLorean");  Check the .to see what else it's got.+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.
  
-//Yeslike printf formatting, so sue me.  I also like std::strings.  So you'll see a lot of those .c_str() conversions around+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 somethingthe effect is nothing happens, not an error
-</code> + 
-** Is it an entity or a database?** +There are a TON of tiny helper functions in **EntityUtils.cpp**, check the h sometime.  My philosophy is if it'simple one line of code to add fadezoom, or bob to a visual, you are that much more likely to do it. 
-This "Delorean" has something powerful under its hood - a Proton VariantDB object.  This is a tiny but powerful name/key database that internally uses stl hashmap for quick look ups of not only variablesbut also dynamic functions Let's store some arbitrary data in our entity.+ 
 +If you'd like to work along with me, open the RTSimpleApp project now!
  
 <code cpp> <code cpp>
-ent.GetVar("gigawatts")->Set(1.21f);  +//Open App.cpp Find MainMenuCreate(pGUIEntin App::Update(), comment it out and add two lines so you have this:
-ent.GetVar("trivia")->Set("Doc pronounced giggawatt wrong in the movie");+
  
-//retrieve the data like 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);
  
-LogMsg("Gigawatts: %.2f, trivia: %s", ent.GetVar("gigawatts")->GetFloat(), ent.GetVar("trivia")->GetString().c_str()); 
 </code> </code>
  
-Great. Notice that to retrieve the value, you have to already know what it is This type safety is important to keep your game bug free If you try GetFloat(but the data inside is a string, it will assert to let you know.+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)
  
-Types the database understands are float, string, uint32, CL_Vector2, CL_Vector3, CL_Rect2f, *Entity, and *EntityComponent.+{{:entity_1.png?|}}
  
-VariantDB'can Save()/Load() to disk as well, I use one for my app preferences in my games.+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'VariantDB (like pos2d, color, scale2d, etcor the component's VariantDB if it is specific to the component(properties like fileName, and framex, framey)
  
-** Adding component **+Add a few more lines of code: 
 +<code cpp> 
 +//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 second after that. 
 +GetMessageManager()->SetEntityVariable(pEnt, 2000, "pos2d", CL_Vec2f(0,0)); 
 +</code> 
 + 
 +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.
  
-Well, we'd better get a visual on the screen before you fall asleep.  Nowthere are a ton of helper functions in EntityUtils.h but we'll do things "manually" a bit so you can get an idea of how things work.+Instead of jerkily moving, we can use an InterpolateComponent to smoothly change one variant value to another.  This works numbersvectors, colors, etc.
  
-We're going to use an "OverlayRender" component to load and draw a picture on the screen.  You can see all the available components in proton/shared/entity. Projects like RTBareBones don't include them so do it yourself if needed.+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.
  
-Each entity can hold an unlimited number of components, or child entities.  Each component also has its own VariantDB inside.  Components check their parent entity for some things (color, position) and their own database for things that the rest of the entity/components wouldn't care about.+Replace with this code:
  
 <code cpp> <code cpp>
-Entity ent("logo"); +//MainMenuCreate(pGUIEnt); 
-EntityComponent *pComp new OverlayRenderComponent(); +AddFocusIfNeeded(pGUIEnt); //so it will render, update, and draw.  Really it just adds a few components. 
-ent.AddComponent(pComp);+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);
 </code> </code>
  
-Waitwe forgot to actually load the image.+The logo now fades insmoothly 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. 
 <code cpp> <code cpp>
-pComp->GetShared("fileName")->Set("game/test.bmp");+//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);
 </code> </code>
  
-Great, so we're ready to render.  If you look at OverlayRenderComponent.cpp, you'll see it registers a Render function like this:+{{:entity_2.png?|}} 
 + 
 +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:
 <code cpp> <code cpp>
 +//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  //register ourselves to render if the parent does
- GetParent()->GetFunction("OnRender")->sig_function.connect(1, boost::bind(&OverlayRenderComponent::OnRender, this, _1));+ 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(); 
 </code> </code>
  
-Ok, don't panic.  What it's doing is saying "When someone calls "Render" dynamically on our parent entity, we'll also run our own render function" It doesn't matter that the Parent doesn't have a "Render" in its function databaseby asking for it, it is created, same as variables work.+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 casesthis kicks in if they don't exist yet.
  
-Soto get this component to run its "Render" (you'd have to do this per frameyou could do:+Look near the bottomit's not using the GetParent() prefix, it's using variables from its own VariantDB.  A uint32 named "frameswith 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:
  
 <code cpp> <code cpp>
-ent.GetFunction("OnRender")->sig_function(&VariantList(Variant(0,0)));+pComp->GetVar("frames")->Set(uint32(20)); 
 +pComp->GetVar("timeBetweenFramesMS")->Set(uint32(20));
 </code> </code>
-That would do it.  Or, you could call it on the component itself: 
  
 +Then compile and you'll get this:
 +
 +{{:entity_3.png?|}}
 +
 +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:
 <code cpp> <code cpp>
-ent->GetComponentByName("OverlayRender")->GetFunction("OnRender")->sig_function(&VariantList(Variant(0,0)));+//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()); 
 +
 + 
 +//Nextadd 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);
 </code> </code>
-This would render the image from a reference point of 0,0. 
  
-Now, let's say you had an entity that had 50 child entities, some of which had additional child entities Wow, that's a lot of OnRender's to call!+{{:entity_4.png?|}}
  
-No worries, you can recursively call the function so it hits all of them:+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:
  
 <code cpp> <code cpp>
-ent. CallFunctionRecursively("OnRender", &VariantList(Variant(0,0))); 
  
-//Nowthe above would workbut a better way would be to pass on each entities frame of reference, so the positions are relative to their parents xy position.+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()));
  
-ent.CallFunctionRecursivelyWithUpdatedVar("OnRender"pVList, string("pos2d"), 0Entity::RECURSIVE_VAR_OP_ADDITION_PLUS_ALIGNMENT_OFFSET);+ int timeToTakeForMoveMS = RandomRange(1001000)
 + ZoomToPositionEntityMulti(pEnt,vPosToMoveTotimeToTakeForMoveMS, INTERPOLATE_SMOOTHSTEP, 0);
  
-//The above says "modify the reference point by the whatever is in the variable 'pos2d'".  Now when you move an entityits children will respect that move when rendering.+ //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)); 
 +}
 </code> </code>
  
-Now, what if instead of calling it, we build a component that called the above and we named it FocusRenderComponent?+And here is the replacement code for the init:
  
-(FocusRenderComponent attaches its OnRender to GetBaseApp()->m_sig_render, a signal that is triggered once per frame when the game is supposed to render.  There are other ones there as wellanyone can attach to them if they care if certain events happen.  Instead of pollingwe wait for things to push data when needed.  This method is sometimes called "signals and slots".+<code cpp> 
 + //MainMenuCreate(pGUIEnt); 
 + AddFocusIfNeeded(pGUIEnt); //so it will render, updateand draw.  Really it just adds a few components. 
 +  
 + int logosToCreate = 10;
  
-So, to sum up, this means you can store entities anywhere, you aren't limited to a single hierarchy.  The engine doesn't care, as long as you add a FocusRenderComponent, it will be rendered at the correct time.+ while (logosToCreate--) 
 + {
  
-Similarly, FocusInputComponent is used to catch touches and keystrokes in OnInputand FocusUpdateComponent is used for recursively run OnUpdate() on all entities You can also setup filters to not pass recursive events down to children if needed.+ //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
  
-That said, there is a handy place for you to store your entities, GetBaseApp()->GetEntityRoot(). (It's an entity you can add to, think of an empty entity as functioning like a folder if you add more entities as children to it)+ //add the change color when touch stuff 
 + pComp = pEnt->AddComponent(new TouchHandlerComponent)
 + pEnt->GetFunction("OnTouchStart")->sig_function.connect(&OnEntityTouched);
  
-** The MessageManager **+ //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
  
-Ok, take everything you've learned and understand that you can "schedulethose things to happen as well.+ pEnt->GetFunction("MoveToRandomPlace")->sig_function.connect(&MoveEntityToRandomPlace);
  
-Here is an example, and now we're going to use helper functions.  Feel free to look at the helper function source, they are just doing easily understood stuff, it's easy to write your own.+ //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)); 
 +
 +</code>
  
-//one time init +{{:entity_5.png?|}}
-Entity *pEnt = CreateOverlayEntity(GetBaseApp()->GetEntityRoot(), "logo", "game/test.bmp",0,0); +
-AddFocussIfNeeded(pEnt); //so it will get render, update, and input calls +
-GetMessageManager()->+
  
 +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.1287985223.txt.gz · Last modified: 2010/10/25 05:40 by seth