Menu

Tree [f0aa37] master /
 History

HTTPS access


File Date Author Commit
 Test_Files 2014-04-19 lastresort92 lastresort92 [f0aa37] Fixed a couple bugs with object destruction. Ma...
 include 2014-04-19 lastresort92 lastresort92 [f0aa37] Fixed a couple bugs with object destruction. Ma...
 lib 2014-04-19 lastresort92 lastresort92 [f0aa37] Fixed a couple bugs with object destruction. Ma...
 LICENSE.txt 2014-02-23 lastresort92 lastresort92 [59f614] Fixed major bug with destroying objects.
 README.txt 2014-04-19 lastresort92 lastresort92 [f0aa37] Fixed a couple bugs with object destruction. Ma...

Read Me

==OCS - ObjectComponentSystem==
----      02/22/2014       ----

OCS provides a fast and easy to use interface for creating, 
testing, and modifying objects, their components, and the systems that allow
everything to interact. 

===================================================================================================================

ABOUT:

	OCS was originally designed for a networked zombie game (<https://github.com/ayebear/undeadmmo>) created
	by a few friends for both a learning experience and for some fun. We came across entity-component-systems
	and were intrigued by the idea because of how different is was from object oriented design. The simplicity
	of creating new entities, coupled with the efficient use of the cache was an interesting concept and knew it
	could speed up our development time as well as reduce the amount of debugging we would have to do later on.
	Working on this library has proven to be an invaluable learning experience and I hope that someone else will
	find it useful.
	
	Although OCS was designed for the above game, it is generic enough that it can be plugged into almost any kind
	of project. Custom components, systems, and messages are very simple to create, and the interface is intuitive
	enough to allow for an easy going experience. This library will continue to improve as more bugs are found and
	more features are needed, so keep an eye out for any updates.

===================================================================================================================

REQUIREMENTS:

	-A compiler with c++11 support. This software was built and tested using gcc 4.7.
	-Cmake version >= 2.8. Can be downloaded from <http://www.cmake.org/>

===================================================================================================================

INSTALLATION:
	
	

===================================================================================================================
			
LICENSING:

	OCS is licensed under the zlib license. See LICENSE.txt for full details.

===================================================================================================================

KNOWN BUGS:

	Program will crash if wrong format is used when loading prototypes.

===================================================================================================================

SUPPORT:
	
	If any bugs are found or if you have any suggestions for new features or changes, 
	you can reach me at KevM1227@gmail.com.
	
	For more information, see the project's home page at
	<http://sourceforge.net/projects/objectcomponentsystem/>

===================================================================================================================

INSTRUCTIONS:

	There are four main parts to OCS: components, objects, systems, and messaging. This section
	will review all of these and show examples on how to use them.
	
===================================================================================================================	

	To jump to a section, highlight the title and press ctrl-c, ctrl-f, ctrl-v.
	
	TABLE OF CONTENTS:
		
		COMPONENTS
			CREATING A COMPONENT
			SERIALIZING A COMPONENT
		OBJECTS
			CREATING OBJECTS
				CREATING A BLANK OBJECT
				CREATING AN OBJECT WITH COMPONENTS
				CREATING A COMPONENT FROM A PROTOTYPE
			ADDING COMPONENTS
			REMOVING COMPONENTS
			DESTROYING AN OBJECT
			QUERYING AN OBJECT'S COMPONENTS
			OBJECT PROTOTYPES
				CREATING A PROTOTYPE IN THE SOURCE CODE
				CREATING A PROTOTYPE IN AN EXTERNAL FILE
				LOADING PROTOTYPES
				PROTOTYPE SETS
					LOADING A PROTOTYPE SET		
		SYSTEMS
			CREATING A SYSTEM
			ADDING SYSTEMS
			REMOVING SYSTEMS
			UPDATING SYSTEMS
				WRITING THE UPDATE FUNCTION
		MESSAGING
			TRANSCEIVERS
			MESSAGES
				CREATING MESSAGES
			LOGGING MESSAGES
				LOGGING A SINGLE MESSAGE
				LOGGING ALL POSTED MESSAGES
				LOGGING ALL PRIVATE MESSAGES
			SENDING MESSAGES
				SENDING POSTING MESSAGES
				SENDING PRIVATE MESSAGES
			READING MESSAGES
				READING POSTED MESSAGES
				READING PRIVATES MESSAGES
			CLEARING MESSAGES
				CLEARING POSTED MESSAGES
				CLEARING PRIVATE MESSAGES
		TIPS

===================================================================================================================
			
	COMPONENTS:
		
		The component is the basis of OCS. They define what objects are and how they interact with
		the rest of the world. A component in OCS is a POD type, and represents a single aspect
		about an object. All components are stored in arrays to limit the amount of cache misses that
		occur in a program. Components and Messages make use of the Curiously Recurring Template Pattern(CRTP)
		to keep track of their ids.
		
		NOTE: No logic other than serializing or deSerializing should be in the components and all components 
			  MUST implement a default constructor and SHOULD implement a parameratized constructor.
			
		
		CREATING A COMPONENT:
		
			A Position component might have 2 float values: an x, and a y. To create this component, you might
			make it like this.
			
			struct Position : public ocs::Component<Position>
			{
				Position(float _x = 0.0f, float _y = 0.0f) : x(_x), y(_y) {}
				
				float x, y;
			};
			
			This component can then be added to an object using the ObjectManager class.
			
		SERIALIZING A COMPONENT*:
		
			All components have access to a Serializer object. This object operates in a similar way to printf and scanf in 
			how it takes a format string and a list of variables, and returns a string in the given format.
			
			To allow a component to be serialized or deserialized (perhaps to be loaded from a file), simply overload the serialize
			and deSerialize functions in your component. Then in the definition, call the serialize or deSerialize function of the 
			Serializer object, and give it a format string and a list of values to serialize.
			
			e.g.
			
			struct Position : ocs::Component<Position>
			{
				Position(float _x = 0.0f, float _y = 0.0f) : x(_x), y(_y) {}
				
				std::string serialize() { return serializer.serialize("% %", x, y); }
				void deSerialize(const std::string& values) { serializer.deSerialize("% %", values, x, y; }
				
				float x, y;
			};
			
			The component can then be converted to and from a string by calling the serialize or deSerialize functions.
			
			*More serializing options are coming soon. Converting to and from byte arrays, and loading arrays into the string are next additions.
	
	===================================================================================================================
	
	OBJECTS:
		
		An object in OCS is represented by an ID and is the element that ties a group of components together. The
		ObjectManager retrieves the object's id and can use that to locate a specific component in the component's
		array.
		
		CREATING OBJECTS:
		
			NOTE: All examples below assume an instance of ObjectManager called objManager exists. If your manager inherits from State, one is given
				  to you.
				  
			Objects can be created in three different ways: as an empty object with no components, an object with one or more components passed
			to the createObject function, or copied from an object prototype.
			
			CREATING A BLANK OBJECT:

					auto id = objManager.createObject();
				
				Creating an empty object should be rarely used, but if you do then it is recommended that you store the id of the 
				object in order to add components on set up. 
				
			CREATING AN OBJECT WITH COMPONENTS:
			
				Assuming components called "Position", "Velocity", and "Renderable" have been created.

					objManager.createObject( Position(45, 75), Velocity(34, 4), Renderable("Some texture") );
				
			CREATING AN OBJECT FROM A PROTOTYPE:
				
				To create objects using a prototype, pass in the name of the prototype. To create 10 objects from
				a prototype called "Player" you might do this:
				
					for(int i = 0; i < 10; ++i)
						objManager.createObject("Player");
						
				All objects created will have the same components with the same values that were given to the prototype on
				creation.
				
		ADDING COMPONENTS:
		
			If a component needs to be added to an object after creation you can do so my calling the addComponents function.
			
			e.g.
				objManager.addComponents( Position(65, 45) );
				
			Multiple components can be added at once. However, keep in mind that an object may only have one instance of each type
			of component for technical reasons.
			
		REMOVING COMPONENTS:
		
			To remove one or more components from an object, call the removeComponents function.
			
			e.g.
				objManager.removeComponents<Position, Velocity>(45);

		DESTROYING AN OBJECT:
		
			The following code will destory an object with the id 4 as well as all of its components.
			
				objManager.destroyObject(4);
				
		QUERYING AN OBJECT'S COMPONENTS
		
			To determine if the object 45 has Position, Velocity, and Renderable components use the following code.
			
				objManager.hasComponents<Position, Velocity, Renderable>(45);
				
			Returns true if the object has all components and returns false if the object is missing at least one of the components.
			
		OBJECT PROTOTYPES
		
			Object prototypes can make creating new objects much cleaner. Instead of specifying the types and values
			of components every time a new object is needed, you can define what components an object should have and
			give it an alias to refer by later. Prototypes can be made directly in the source code or in a separate file.
			
			CREATING A PROTOTYPE IN THE SOURCE CODE
			
				To create an object prototype, call the addComponentsToPrototype function in ObjectManager and specify the
				name of the prototype to add the components to. If the prototype does not already exist, one will be created.
				
					objManager.addComponentsToPrototype("Player", Position(65, 45), Velocity(200, .5), Renderable("Txtre File.png");

				This will create a prototype under the name "Player" with Position, Velocity, and Renderable components.
				
				Any objects that are created from this prototype will be given the same components with the initial values.
				
			CREATING PROTOTYPES IN AN EXTERNAL FILE
				
				The ObjectPrototypeLoader class allows you to easily create and test new types of objects by defining them in
				an external file. Defining the prototypes in a file prevents the program from recompiling whenever a change
				to a prototype is made. Multiple prototypes are able to be loaded in one function call to keep the source code clean.
				
				REQUIREMENTS:
					
					-All used components must implement a deSerialize function.
					-The values for the components must be in the format that was specified in the deSerialize function.
					-All used components must be bound to a string so the prototype loader can determine the type of the component.
						objManager.bindStringToComponent<Position>("Position");
		 	
				A prototype in a file has three main parts:
					-The Section header which is the name of the prototype.
					-An array of component names
					-The values of each component.
					
					Every section must have a section header and an end tag. These markers are enclosed in square brackets('[' , '/]').
					
					A prototype called "Player" would have a section that looks like this.
					
						[Player]
						
						[/Player]
					
					The component names and values would go in between these markers.
					
					Every prototype section should have an attribute call "Components". This attribute will contain an array
					of component name that define what components the prototype has. All components and attribute are enclosed
					in double quotes, and an array is a comma separated list of attributes enclosed in curly braces ('{' , '}').
					
					A colon separates an attribute name from its values.
					
						[Player]
							
							"Components" : { "Position", "Velocity", "Renderable" }
							
						[/Player]
					
					Each component name should then have an attribute with the same name as the one that is in the list.
					Each attribute will then have an a single attribute, a string in the format that was specified in the serialize function.
					
					Assuming Position and Velocity take the format "% %", and Renderable takes the format "%s".
					String values should be enclosed in single quotes.
					
						[Player]
							
							"Components" : { "Position", "Velocity", "Renderable" }
							
							"Position" : "65 45"
							"Velocity" : "200 .5"
							"Renderable" : "'Txtre File.png'"
							
						[/Player]
					
					After this prototype is loaded, it can be used as if it was hard coded. There is no performance difference after the
					initial set up.
					
					LOADING PROTOTYPES
					
						Before loading a prototype, you must bind your components to a string.
						
						Somewhere before we load our player prototype, we must bind the Position component to the string "Position",
						the Velocity component to the string "Velocity", and the Renderable component to the string "Renderable".
						The associated words do not need to be named the same as the component. It is just recommended to keep
						everything consistent.
						
							//In the configure function of the State class, or somewhere else.
							objManager.bindStringToComponent<Position>("Position");
							objManager.bindStringToComponent<Velocity>("Velocity");
							objManager.bindStringToComponent<Renderable>("Renderable");
		
						Now we can load our player prototype.
						
						The loadObjectPrototype function is static within ObjectPrototypeLoader.
						It takes an ObjectManager reference, a file path, and the name of the prototype as parameters.
						
							//Assuming our player prototype is in "player.txt" and it is in the root folder.
							ObjectPrototypeLoader::loadObjectPrototype(objManager, "player.txt", "Player");
						
						The "Player" prototype can then be used to create objects.
						
						Please note that there may be more than one prototype per file, but each one must have a unique name.
						
				PROTOTYPE SETS
				
					A prototype set is a list of prototypes that can simplify the loading of prototypes.
					
					To load a prototype set, create a section in the same manner as above, and create an attribute
					with a value that contains an array of the prototype names.
					
						[Set]					
							"Prototype Set" : { "Player", "Ball", "Brick" }					
						[/Set]
						
					Then somewhere else in the file would be the definitions of the prototypes. These are defined the same way
					as in the previous section.
					
					LOADING A PROTOTYPE SET
				
						Like the loadObjectPrototype function, loadPrototypeSet is static within ObjectPrototypeLoader.
						
						This function takes an ObjectManager reference, the file with the prototype set and definitions,
						the name of the prototype set attribute, and the section that this attribute resides in.
						
							//Assuming the above example is in prototypes.txt and all prototypes are defined
							ObjectPrototypeLoader::loadPrototypeSet(objManager, "prototypes.txt", "Prototype Set", "[Set]");
							
						Notice the square brackets around the name of the section.
						
						Calling this function would be the same as calling loadObjectPrototype for all 3 prototype names.
				
			
	
	===================================================================================================================
	
	SYSTEMS:
		
		The core of a system is a single update function that is called every frame. This is the logic of the program that allows components to
		interact. A system usually operates on a single component's array and would fetch needed information from the ObjectManager.
		
		CREATING A SYSTEM:
			To create a system, simply inherit from the System class and implement an update function that takes an ObjectManager reference,
			a MessageHub reference, and a double for the elapsed time.
			
			The following code implements a movement system that operates on objects with Velocity components. This assumes that velocity has
			two floats called "speed" and "angle".
			
				struct MovementSystem : public ocs::System
				{
					void update(ObjectManager& objManager, MessageHub& msgHub, double dt)
					{
						//Iterate through ALL velocity components. Notice we are taking in a reference. We do this so we can
						//modify the actual component.
						for( auto& vel : objManager.getComponentArray<Velocity>() )
						{
							//Use the velocity component's owner id to get the object's position component
							auto pos = objManager.getComponent<Position>( vel.getOwnerID() );
							
							//Check if the object actually had a Position component.
							if(pos)
							{
								Use the velocity component to update the object's position.
								pos->x += cos(vel.angle) * vel.speed * dt;
								pos->y += sin(vel.angle) * vel.speed * dt;
							}
						}
					}
				};
			
			Systems can use the MessageHub to post messages if anything interesting occurs (Such as a collision). See Messaging section.
			
		ADDING SYSTEMS:
		
			NOTE: Each SystemManager can have one instance of each type of system for technical reasons. (Why would you need more?)
			
			Assuming a system call MovementSystem has been created, and a SystemManager called sysManager is available,
			add it to the SystemManager thusly:
			
				sysManager.addSystem<MovementSystem>();
			
			If systems are updated using the updateAllSystems function in the SystemManager, then systems will be updated in the
			order that they were added in. A system can be manually updated by called the updateSystem function and passing in the
			system as the template paramater.
			
				sysManager.updateSystem<MovementSystem>(dt);
		
		REMOVING SYSTEMS:
			
			Removing Systems is as easy as passing in the system type to the removeSystem function.
			
				sysManager.removeSystem<MovementSystem>();
				
		UPDATING SYSTEMS:
			
			All systems are, in essence, a single update function. This function should operate a single component array and be as
			decoupled as possible. Every update function has access to an ObjectManager, MessageHub, and the time elapsed from the
			previous frame.
			
			Systems can be updated in two ways: update all systems in a single function call, or individually.
			
			If updateAllSystems is called, then the systems will be updated in the order that they were added in.
				
				sysManager.updateAllSystems(dt);
				
			You can also call updateSystem and pass in the System type as a template paramater to manually update a system.
			
				sysManager.updateSystem<MovementSystem>(dt);
				
			WRITING THE UPDATE FUNCTION:
			
				An update function for a movement system might look like this:
				
				//Assuming components call Velocity(an angle and speed) and Position(an x and a y) have been created
				void MovementSystem::update(ObjectManager& objManager, MessageHub& msgHub, double dt)
				{
					for(auto& vel : objManager.getComponentArray<Velocity>())
					{
						//Attempt to get the velocity's owner's position component
						auto pos = objManager.getComponent<Position>(vel.getOwnerID);
						
						//Ensure that the object has a position component. 
						if(pos)
						{
							pos->x += cos(vel.angle) * vel.speed * dt;
							pos->y += sin(vel.angle) * vel.speed * dt;
						}
					}
				}
	
	===================================================================================================================	
	
	MESSAGING:
	
		TRANSCEIVERS:
			
			A transceiver in this context is anything that can send or receive messages through the MessageHub.
			
			By default, all States and systems are transceivers. If there is anything else that you need to use
			the MessageHub, you can make that object inherit from the Transceiver struct.
			
			To use the MessageHub, you must have access to a Transceiver object, either through inheritance or composition.
			Inheriting from Transceiver allows the creator to pass "*this" to the message functions. However, there is no
			difference otherwise.
			
		MESSAGES:
		
			A message is similar to a component in that it is a POD type. However, messages can be posted such that
			it is available for all other transceivers to see. A message can also be sent privately to another transceiver
			if the receiving ID is known.
			
			CREATING MESSAGES:
			
				Creating Messages is very similar to creating a component. Simply inherit from "Component" and pass in the new class type as
				a template paramater.
				
				Give the message a constructor that takes a const Transceiver reference, which will be the message creator, and any
				other paramaters that are needed to set up the message.
				
				All messages must take a const Transceiver reference an pass the transceiver to the Message base class constructor.
				
				e.g.
				
					struct ObjectDestroyed : public Message<ObjectDestroyed
					{
						ObjectDestroyed(const Transceiver& transceiver, ocs::ID _destroyedObjectID) : 
							Message(transceiver), destroyedObjectID(_destroyedObjectID) {}
						
						ocs::ID destroyedObjectID;
					};
					
				The message can then be posted to be viewed publically or it can be sent in a private message.
				
			LOGGING MESSAGES:
				
				Messages can be logged to any ostream object such as the console window or an open file.
				
				All messages have default logging which contains the Message Family, and the sender id. To gain access to more
				specialized logging overload the "log" function in your message object and define how the message should be logged.
				
					struct ObjectDestroyed : public Message<ObjectDestroyed
					{
						ObjectDestroyed(const Transceiver& transceiver, ocs::ID _destroyedObjectID) : 
							Message(transceiver), destroyedObjectID(_destroyedObjectID) {}
						
						void log(std::ostream& out)
						{
							out << "Message Type: ObjectDestroyed\n";
							out << "Message Family: " << getFamily() << std::endl;
							out << "Sender: " << getSender() << std::endl;
							out << "Destroyed Object ID: " << destroyedObjectID << std::endl;
						}
						
						ocs::ID destroyedObjectID;
					};
				
				Ideally, the logged message should include the Message Type(The name of the message object), the message family(The family id
				that was assigned to the message), the transceiver id of the sender, and all appropriate information about the message, all with labels.
				
				LOGGING A SINGLE MESSAGE:
				
					To log a single message, call the log function on an instantiated message object and call log and pass in an ostream object.
				
						ObjectDestroyed objDestroyed(*this, 5);
						objDestroyed.log(std::cout);
					
					Messages will rarely be created in this manner as the MessageHub should handle all message creation.
					
				LOGGING ALL POSTED MESSAGES:
				
					To log all messages that are currently posted in the MessageHub, call logPostedMessages and pass in an ostream object.
			
						msgHub.logPostedMessages(std::cout);
					
				LOGGING ALL PRIVATE MESSAGES:
				
					To log all of a transceiver's private messages, call logPrivateMessages and pass in the transceiver object and an ostream object.
					
						msgHub.logPrivateMessages(*this, std::cout);
			
			SENDING MESSAGES:
			
				SENDING POSTING MESSAGES:
				
					Messages are able to be posted publically for all transceivers to view. To post a message, pass in the message type as
					a template paramater, and then pass in the creating transceiver and any paramaters for the message.
					
					e.g. Assuming a message called ObjectDestroyed has been created, the poster inherits from Transceiver, and a MessageHub called
					msgHub is available.
						
						//Post that object with id '5' has been destroyed
						msgHub.postMessage<ObjectDestroyed>(*this, 5);
					
					The message hub will then create a new ObjectDestroyed object with the given paramaters, and other Transceivers can view it
					by using the readPostedMessages function.
					
				SENDING PRIVATE MESSAGES
				
					All transceivers are given access to a "mailbox" that other transceivers can send messages to. Sending private messages
					is nearly identical to posting a message. However, you must know the transceiver id of the receiver.
					
					The following code assumes that a "TextMessage" message has been defined that takes a transceiver object and a single string argument.
					Also assumes that the sender inherits from "Transceiver".
					
						//Send a TextMessage to transceiver with id '4'
						msgHub.sendPrivateMessage(4, *this, "Hello Transceiver 4!");
			
			READING MESSAGES:
			
				READING POSTED MESSAGES:
				
					The MessageHub acts as a bulletin board that is organized by the types of the messages. All transceivers can view these posted messages.
					
					To get a list of all messages of a certain type, pass in the message type as a template paramater to the readPostedMessages function.
					
						//Get a list of all TextMessage messages and store them in textMsgs.
						auto textMsgs = msgHub.readPostedMessages<TextMessage>();
						
					Returns a vector of all posted messages of the given type. It is recommended to create a variable using auto as the return type
					may change in future versions (i.e. vector -> list etc.).
					
					Reading posted messages has no effect on the actual message board.
					
				READING PRIVATE MESSAGES:
				
					Reading private messages is nearly the same as reading posted messages. The only difference is you give the function your
					Transceiver object to gain access to your mailbox.
					
						auto privateTxtMessages = msgHub.readPrivateMessages<TextMessage>(*this);
			
			CLEARING MESSAGES:
			
				CLEARING POSTED MESSAGES:
				
					If the message board is not cleared periodically, it can become cluttered and slow. Therefore, it is recommended to call
					clearPostedMessages every frame in the main loop.
					
					An automatic solution is in the works.
					
				CLEARING PRIVATES MESSAGES:
				
					If you wish to clear your personal mailbox, call clearPrivateMessages and pass in your Transceiver object.

TIPS:

	-Post any user input to the MessageHub and allow the systems to determine what to do with it.
	-Use the new ranged based for loops to step through the component arrays. 
		-for(auto& renderComp : objManager.getComponentArray<Renderable>())
	-Use the configure function in the State class to create any object prototypes.
	-Prefer to load prototypes from a file to allow for faster modifying of prototypes.
		-The program will not need to be recompiled if an external file is modified.
	-Use the initialize function in the State class to create actual objects and set their starting position.
	-Call 'clearPostedMessages' for the message hub every frame to prevent messages from piling up.
	-Try to avoid creating systems that rely on other systems or components that rely on other components. The more decoupled 
		everything is the better.
	-Email me anymore tips, suggestions, or bugs as you find them.
===================================================================================================================
Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.