-1
\$\begingroup\$

I'm designing a game engine in C++, and I ran into a problem with my messaging system.

The "RequestData, ResultData" system is very appealing to me, because I only have to create a ResultData object, call the send message method (calling functions in chain while request is not fulfilled), and then use the ResultData object.

For example, there is a GameObjectResultData object, which will store game object pointers which fulfill a criteria. This can be used to get one object, two objects, etc.


I have a GameObjectRequestData, which has the following members: 1. int requestedNum: the number of game objects to be found, so the request stops broadcasting after the right number is found. 2. GameObjectResultData: the resulting data objects pointer, in which the result will be stored.

I have to store the criteria in the request, without creating a lot of different RequestData classes. For examples, IsNameEquals(string), IsTagEquals(string), IsInRangeOfPoint(point, range).

When the RequestData object arrives at a GameObject object, how do I check the criteria?


My current idea is to use a SearchCriteria abstract class and a SearchCriteriaType enum, which will both be stored in the RequestData. For example, I could use a NameEqualsCriteria class and a NAME_EQUALS_CRITERIA enum. In this case, when the RequestData arrives at GameObject, I check the enum value, and dynamically cast the SearchCriteria object to the corresponding class.

if(type == NAME_EQUALS_CRITERIA)
    dynamic_cast<NameEqualsCriteria>(SearchCriteria);
    // then, check criteria.

I think it should work, but I am not so experienced, so I am not sure. Is this a good practice?


I think there is an overhead on creating the new enum and class types. How do I set up the RequestData.SearchCriteria object, in the simplest form?

If I use the name criteria, I have to set a string; if I use the range criteria, I have to set a point and a range. My current solution is to pass the enum to the RequestData constructor, which will create a corresponding SearchCriteria object. Then, back where I called the constructor, I dynamically cast the SearchCriteria object to the corresponding class, and call its setters.

How do I properly set criterias, when sending search requests in messaging system?

\$\endgroup\$
  • 1
    \$\begingroup\$ This sounds really overengineered to me. What is the basis for your creating such a generalized message targeting system? \$\endgroup\$ – Josh Dec 20 '16 at 16:38
  • \$\begingroup\$ Well, the game objects in the scene communicate with messages. GameObject A sends to it to scene, then the scene finds the gameobject/broadcasts it etc. I use the same "channel" for every kind of messages which is between the scene and a gameobject or between gameobjects. \$\endgroup\$ – Tudvari Dec 20 '16 at 16:41
  • \$\begingroup\$ By why go to the extreme of essentially building what appears to be a query language for your game objects? It seems entirely unnecessary to me, and thus adds a lot of overhead in the transmission of any message. So I'm curious if there is a particular scenario you have where this kind of generalized dynamic message targeting is a benefit. \$\endgroup\$ – Josh Dec 20 '16 at 16:43
  • \$\begingroup\$ oh nononono, that was just a way how I expressed myself. (if you are talking about the "type == ... then ..." thing. \$\endgroup\$ – Tudvari Dec 20 '16 at 16:50
  • \$\begingroup\$ The core of my messaging system would be: The sender sends a request with criteria and a pointer to the place where the result will be put, and then the elements of the system pass this request through the system to the recipent, where the recipent puts the info to the place where the pointer points in the request. (simplifed) \$\endgroup\$ – Tudvari Dec 20 '16 at 16:52
1
\$\begingroup\$

I think this concept you're describing of building a query language mechanism to match targets of a message is a bad idea. It seems over-engineered and too generic to really suit the domain, especially given the processing overhead required to evaluate these abstract queries.

I think you should opt for a simpler approach.

Any message system can be placed into one of two buckets: a system where the sender identifies the recipients in some way, or a system where the recipients decide if they care about a message.

Your system appears to be basically in the former category: the sender fills out the criteria that must be true for an object to process the message. This means that the sender must, perforce, assume some things about the potential destination types -- that they have certain properties to be queried. In exchange for the downside of having to make this assumption, you have the upside of not having to duplicate "do I care about this message?" logic in different types of target game object.

Instead of trying to build an interpreted, scripted query language over your game objects, why don't you just perform the queries in your native programming language?

When the sender prepares a message, instead of building a query in a DSL, simply give the message a querying function that takes a GameObject* and returns a boolean value. So your message might look like:

struct Message {
  std::function<bool (GameObject*)> query;
  // ...other stuff...
};

When you "send" this message, you essentially do:

for (GameObject* object : allGameObjects) {
  if (message.query(object)) {
    object->handleMessage(message);
  }
}

(Instead of directly calling handleMessage you can store the matching objects in a vector or what have you for later processing, if you like.)

That way you can encapsulate any query you could write in C++ in your messaging system, and it will be significantly more natural to read or write, and significantly faster to execute, than any sort of DSL approach.

ExplodeMessage message;
message.query = [](GameObject* subject) {
  // any object whose name is "bomb" will process this message
  // and blow up
  return subject->GetName() == "bomb";
}

sendMessage(message);

Now you might argue that this approach requires you to make all the properties you might want to query against part of the public interface of the base game object, and that's true, but that's not fundamentally different from before. Before you were positing querying against properties that are effectively public since you have to know about (or assume) their existence to write the query operands. Now you're simply formalizing that assumption in a more naturally C++ fashion.

\$\endgroup\$
  • \$\begingroup\$ I will read it soon, but first: I didn't try to build an interpretered language. I just didnt want to write full program code. \$\endgroup\$ – Tudvari Dec 20 '16 at 17:47
  • \$\begingroup\$ This is a good approach, and yes it's faster than interpretered version, but I never intended to use them anyway. Is this fast enough for an engine? What I was talking about only passes a pointer to the "message" and the type of the message. The recipents dynamic cast the message based on the type, then use the message object the evaulate the criteria, and do something based on that. And when all of this is over, the message was created by sender is filled with info and ready to process. \$\endgroup\$ – Tudvari Dec 20 '16 at 17:59
  • \$\begingroup\$ dynamic_cast is almost always a hint that you're doing it wrong, or at least sub-optimally. I'd strongly encourage you to try to find a solution that doesn't require it. \$\endgroup\$ – Josh Dec 20 '16 at 18:13
  • \$\begingroup\$ This should be "fast enough" for general use, although the whole idea of a message-passing system will incur some overhead; I personally find them distasteful. You can invert the logic like you're talking about: pass the message to the objects who will directly evaluate if they care about the message. This can be slightly faster due to less overhead (no std::function to cart about), but has the cost of potentially having to repeat code for dealing with various queries. \$\endgroup\$ – Josh Dec 20 '16 at 18:15
  • \$\begingroup\$ Now I really starting to like std::function. One question: I want to assign a function the query member variable inside another class' method and I want to make that function a separate function with a name. (so I can reuse it in different messages). Where should I put that? Is it a good practice to put it into a GameObjectQueries.cpp then just include that? (and ofc include GameObject.h) \$\endgroup\$ – Tudvari Dec 20 '16 at 23:59
-1
\$\begingroup\$

The most generic way to do it would be to store your criteria as strings, or hashed strings, define a set of comparison operators, and use a variant type.

Variant
  enum AvailableTypes
    TYPE_INT
    TYPE_FLOAT
    TYPE_STRING

  AvailableTypes mType;

  union {
    int;
    float;
    string;
  } mData;

enum Operator
  EQUAL
  GT
  LT
  GT_EQ
  LT_EQ


RequestData
  string mCriteriaParam;
  Operator mOp;
  Variant mOpParam;


GameObject
  map<string, Variant> mObjectData; // the key is the name of the parameter (tag, 
                                    // currentHealth, name, maxHealth)

When the RequestData arrives at your GameObject, you search the mObjectData for the mCriteriaParam, use the operator and compare the mObjectData to the mOpParam.

This design looks generic, but might be inefficient. And this is effectively recoding a small subset of SQL.


There are a couple of ways to make variant types:


There are ways to organize your code to use either std::strings or integer values for keys in these map. You chose at compile time. If searching through strings makes your game slow, you could try switching to hashed-to-int versions of these.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.