QSDK 1.1 Documentation

Programmers Tutorial: Unit 78

Purpose: Declaring and defining entities. Some entities are defined, created and their behaviors defined.
Concepts introduced: Q::Entity, Q::EntityInstance, Q::EntityBase, Q::EntityFactory
Samples: entityt2, factory and factory2

The following includes are required for the code snippets:

#include <iostream>
#include <Q/q.h>
#include <Q/com.h>
#include <Q/gamedev.h>

Declaring entities

Q::Concept cat("cat");
Q::Group g = Q::Group::create(0, Q::Zone("garden").root());
Q::EntityInstance2::PTR e = Q::EntityInstance2::create("alice", g, cat, Q::EntityInstance2::NotMoving);

Creating the instance of an entity is exactly like creating any Q instances. Instances may have a name and are associated with a group which uniquely locates them in the Q space. On top of that, entity instances come with a concept that represents the (user) type of the entity. In the example, the instance declares that an entity exists with the type cat. This type is only meaningful to the user and does not mean anything to Q. The number of entities is not limited but having all entities active at the same time may be very inefficient. For this reason, not all entities are active: only the ones that may affect the user are active and the others sleep waiting for their tome to come. Entities are activated when the user explicitly requests it or when the geometry associated with it is in scope. This " sensitive" geometry is defined as follows:

Q::EntityInstance::PTR e = Q::EntityInstance::find("alice");
e->add(Instance("alice-head"));
In this example, the entity alice will be activated when the instance alice-head is in scope. Entities may have no instances of that kind, meaning they can only be activated explicitly, or more than one, meaning they are activated when at least one of those instances is in scope.
Q::EntityInstance::PTR e = Q::EntityInstance::find("alice");
e->add(Q::Instance("alice-hand1"));
e->add(Q::Instance("alice-hand2")); 

Defining the behavior of entities

After having declared the existence of the entity, the user must provide the code that will simulate it. All entities implement the interface Q::Entity. When the activation of an entity is requested, this interface describes how it behaves throughout its lifecycle. We store the associated Q::EntityInstance when activated so that it can easily access the data of the entity we simulate. It is important to understand that an entity is instantiated only after Q::Entity::embody has been called and before Q::Entity::disembody has. Therefore the name, the group and all entity-specific data is invalid outside this range and in particular in the constructor and the destructor.

struct Cat: public Q::Entity,
            public Com::ComObject<Q::Entity, Cat>
{
  // COM Object Declaration
  QCOM_DECLARE_COMPONENT;


  Cat()
  {
    // I am a cat but I do not know which one. Therefore
    // my name or my group are invalid

    std::cout << "cat created" << std::endl;
  }

  ~Cat()
  {
    std::cout << "cat destroyed" << std::endl;
  }

  virtual bool embody(Q::EntityInstance* instance)
  {
    // attach the simulated entity instance
    instance_ = instance;

    std::cout << "cat " << instance_->name() << " is embodied" << std::endl;

    return true;
  }

  virtual void disembody(bool destroyed)
  {
    std::cout << "cat " << instance_->name() << " is disembodied" << std::endl;

    // detach the simulated entity instance
    instance_ = 0;
  }
 

  virtual void tick(float duration)
  {
    std::cout << "cat " << instance_->name() << " is ticked" << std::endl;
  }

  virtual void entered(unsigned int, Q::EntityInstance*, unsigned int)
  {
    std::cout << "cat " << instance_->name() << " got entered" << std::endl;
  }

  virtual void exited(unsigned int, Q::EntityInstance*, unsigned int)
  {
    std::cout << "cat " << instance_->name() << " got exited" << std::endl;
  }

  virtual void receive(Q::MessagePeer* from, Q::Message*, Q::MessageResult*)
  {
    std::cout << "cat " << instance_->name() << " received a message" << std::endl;
  }

private:
  Q::EntityInstance::PTR    instance_; // the simulated entity instance 
};

The example above defines the class of a cat. The constructor builds the data common to all cats but not specific to one cat in particular as the entity is not instantiated yet. Then the first method that will get called is Cat::embody(..) when a cat is activated. This method describes what should be done on activation. After that, the method Cat::tick(..) will be regularly called until the entity is deactivated. This method is truly the heart beat of the entity and mostly defines how it behaves. This stops only for two reasons. Either the entity is destroyed explicitly by the user (instance destroyed, i.e. the entity will never come back) or is deactivated because there is no longer a need to simulate it (scope). At this moment, Cat::disembody(..) is called and specifies the cause of the deactivation (destruction or simple end of simulation).

EntityBase

Storing the Q::EntityInstance object the entity simulates while it is active and dropping it on deactivation is an obvious pretty standard Q::Entity implementation. To save the user some of his/her time, <Q/gamedev_helpers.h> provides the helper class Q::EntityBase provides a base implementation of Q::Entity that encapsulates the entity instance. It confuses the entity and its instance in order to be simpler and will be what is needed in most simple cases. Let's rewrite our previous example:

// requires <Q/gamedev_helpers.h>

struct Cat: public Q::EntityBase
{
  Cat()
  {
    // I am a cat but I do not know which one. Therefore
    // my name or my group are invalid

    std::cout << "cat created" << std::endl;
  }

  ~Cat()
  {
    std::cout << "cat destroyed" << std::endl;
  }

  virtual bool embody(Q::EntityInstance* instance)
  {
    // do not forget to call the parent class
    Q::EntityBase::embody(instance);

    std::cout << "cat " << name() << " is embodied" << std::endl;

    return true;
  }

  virtual void disembody(bool destroyed)
  {
    std::cout << "cat " << name() << " is disembodied" << std::endl;

    // do not forget to call the parent class
    Q::EntityBase::disembody();
  }
 

  virtual void tick(float duration)
  {
    // do not forget to call the parent class
    Q::EntityBase::tick(duration);

    std::cout << "cat " << name() << " is ticked" << std::endl;
  }

  virtual void entered(unsigned int, Q::EntityInstance*, unsigned int)
  {
    std::cout << "cat " << name() << " got entered" << std::endl;
  }

  virtual void exited(unsigned int, Q::EntityInstance*, unsigned int)
  {
    std::cout << "cat " << name() << " got exited" << std::endl;
  }

  virtual void receive(Q::MessagePeer* from, Q::Message*, Q::MessageResult*)
  {
    std::cout << "cat " << name() << " received a message" << std::endl;
  }
};

Entity Factories

Now we have created a cat instance and a cat class, the last thing to do is to glue them together. That is what the entity type is there for. This type is a concept that identifies the kind of entity represented by the instance (here, cat). It tells the user what class Q needs when it requests the activation of an entity. The objects that provides the entities on request are called the EntityFactories.

struct MyFactory: public Q::EntityFactory,
                  public Com::ComObject<Q::EntityFactory, MyFactory>
{
  // COM Object Declaration
  QCOM_DECLARE_COMPONENT;

  static void create(Q::EntityFactory** res)
  {
    *res = new MyFactory();
  }


  // Q::EntityFactory features

  virtual Utils::Result get(const Q::Concept& type, Q::Entity** res)
  {
    if (type == Q::Concept("cat")) {
      *res = new Cat();
      return Utils::Success;

    } else {
      // unknown entity type
      return Utils::Failure;
    }
  }

  virtual Utils::Result recycle(Q::Entity*)
  {
      return Utils::Success;
  }
};

The method Q::EntityFactory::get is called when Q requests the activation of entities. The concept passed as a parameter is the type of the activated Q::EntityInstance. The factory defined above only returns the Cat class for entities with the type cat. The method Q::EntityFactory::recycle is called when the entity is deactivated and therefore the Entity object is no longer needed. Because this factory example creates entities on request, there is nothing to do here. The Cat entity is now ready for use. A Cat class is defined. A factory returns Cat objects if requested. Let’s register this factory so that Q knows who to ask when Cat entity instances are activated.

// create the factory
Q::EntityFactory::PTR factory;
MyFactory::create(&factory);

// register the factory
Q::EntityManager::theManager()->addFactory(factory);

Factories may create several types of entities.

struct MyFactory: public Q::EntityFactory,
                  public Com::ComObject<Q::EntityFactory, MyFactory>
{
  // COM Object Declaration
  QCOM_DECLARE_COMPONENT;

  // Q::EntityFactory features

  virtual Utils::Result get(const Q::Concept& type, Q::Entity** res)
  {
    if (type == Q::Concept("cat")) {
      *res = new Cat();
      return Utils::Success;

    } else if (type == Q::Concept("dog")) {
      *res = new Dog();
      return Utils::Success;

    } else if (type == Q::Concept("bird")) {
      *res = new Bird();
      return Utils::Success;

    } else {
      return Utils::Failure;
    }
  }
};

If the factory fails to create an entity for a particular type, it does not mean that the creation of this entity will eventually fail. More than one factory may be registered and Q will request the entity to each of them until a valid one is returned. The order in which Q calls the factories is the order in which they have been registered. Therefore if several factories are able to create an entity of a given type, these entities will actually come exclusively from the first registered one.

Q::EntityManager::theManager()->addFactory(factory1);
Q::EntityManager::theManager()->addFactory(factory2);
Q::EntityManager::theManager()->addFactory(factory3);

Last, just as Q::EntityBase provides the simplest implementation for Q::Entity, Q::EntityFactoryBase provides it for Q::EntityFactory and can be used as a base class for most simple entity factories. It requires including <Q/gamedev_helpers.h>.

A closer look at the life of the cat

Let’s create an entity instance for a cat called alice.

Q::Concept cat("cat");
Q::Group g("alice");
Q::EntityInstance2::PTR e = Q::EntityInstance2::create("alice", g, cat, Q::EntityInstance2::NotMoving);
e->add(Q::Instance("alice-head"));

The instance is attached to a pre-existent group with the exact same name. We consider the simple case where the geometry is already present in the scene. Its head is used to trigger its activation.

At the moment, the cat does not actually exist. The instance is there but no entity is simulated because the zone is out of scope. Let’s add a scope on some geometry in the zone.

Alice’s head has now entered the scope and therefore Q requests the activation of an entity of the type cat. It then calls the factory to get one, which it does because this type is matched by the factory (Note that the factory example constructs the entity at this point. This is not necessary. Factories may be pools of entities that precreate objects and then allocate/deallocate them on activation). At this moment, the Cat entity is activated and thus Cat::embody(..) is called. This method is always the first method being called in the entity lifecycle. From now on, an entity with the type Cat is being simulated on the group ".

The cat is now active and Q will regularly tick it by calling Cat::tick(..). Whether the entity is ticked or not and the frequency of those ticks can be set via the Q::EntityInstance. Let’s now remove the scope from this zone. It will gradually go out of scope and Q will realize that the cat no longer needs being simulated. The entity is deactivated. The method Cat::disembody(..) is called in order to notify the object that it is going to be put to sleep. This does not mean the entity is destroyed; this only means the entity is no longer simulated but the instance still exist (and the cat will be activated again if it goes into scope again). Then Q calls Q::EntityFactory::recycle() on the factory that provided the entity to tell it the object is no longer needed.

The process described above is the usual entity activation/deactivation process. Scopes act as spotlights on the Q space that highlight the active part of it. However some entities may sometimes need to be activated outside the scope. For instance, a switch (in the scope) may call a lift (out of scope) when pressed. The lift must move but the user may not want to put it into scope. the solution is to force the activation of the entity.

void Switch::onPressed()
{
  Q::EntityInstance::PTR i = Q::EntityInstance::find("myLift");

  // force the activation
  i->embody();

  // get the lift entity
  Lift::PTR lift = (Lift*) (Q::EntityInstance*) i->entity();

  // move the lift
  lift->move();
}

The user retrieved the instance with the name myLift (l.3) (the instance always exist. Only the entity gets activated and deactivated) and then calls Q::EntityInstance::embody() (l.5) in order to make sure the entity is active. Then, assuming factories are able to provide Lift entities, the lift itself can be accessed and is certified to be active and valid.

Activation and geometry

In the previous example, the geometry of the entity was assumed to be part of the scene. However it may be more efficient for some entities like animated characters to have "independent" geometry. When the entity becomes active, its actual geometry is either retrieved from a storeroom or cloned from some character model and then attached to its group.

bool Character::embody(Q::EntityInstance* instance)
{
  // get the geometry
  Q::Group g = GeometryPool::thePool()->get("myModel");

  // attach the geometry
  g.parent(group());

  return true;
} 

void Character::disembody(bool destroyed)
{
  // return the geometry back
  GeometryPool::thePool()->recycle(group());
}

The example above illustrates how to deploy geometry stored in a pool on activation. This technique allows to separate the "scenery" geometry of the scene and its "actors". The only problem having the geometry deployed is that instances are also used to trigger the activation of entities by the scope. If there is no geometry to represent the entity in the scene, this one will never be activated. The solution is either to create some dummy geometry to represent its start position or to use some scenery close to it: 

Q::Concept cat("cat");
Q::Group g("alice");
Q::EntityInstance2::PTR e = Q::EntityInstance2::create("alice", g, cat, Q::EntityInstance2::NotMoving);

// create a region to trigger alice’s activation
Q::Region r = Q::Region::create(0, Box3f(0,0,0,0,0,0));
Q::Instance i = Q::Instance::create(0, g, r);
e->add(i);

Separating the geometry and the instance, and using self-contained cloneable animated geometry for characters in the scene are techniques best enhanced using the Puppet class.

Return to QSDK documentation Contents page. Contact details for support, information and fault-reporting.
Qube Software Limited © 2000-2004