• Advertisement
Sign in to follow this  

Extendable inventory and world objects

This topic is 2777 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

So quite every game has a set of objects and features and usually you can find some parts like:

void useInventoryObject(int obj, int count)
{
if(obj == APPLE)
addParam(ENERGY, 20);
else if(obj == MEAT)
addParam(ENERGY, 50);
else if(isClothes(obj))
wearClothes(obj);
}

Because the game designed with some abilities and needs no changes. Maybe some constants can be in config file, like APPLE_ENERGY = 20, MEAT_ENERGY = 50 etc.

In my project I try to make some extendable model to allow external programmers to add some features, objects, or just objects' skins.

For example you want to add some gun. You can write some data in some file, eg. "model=gun23.mod, power=23, bullets=simple, weight=21".

Another problem is that there are many general object types:
1. Parameter-change objects. Like medicine, fruits, vegetables. So on create another object you just write in config "Name=Potato, param_to_change=Hunger, amount=30, skin=potato23.mod".
2. Guns. Already mentioned - you define model, power, what bullets to use etc.
3. Tables. Objects that can be positioned and used for place for some food.
4. Cars-keys. You click and car arrives.
5. Unique portraits. You can uppload some picture and then place it in your room.

This list usually consists of about 50 different objects classes. There is no problem to make list of 50 static functions to create objects when they are read from database, or first creation.

The problem is how to manage them in inventory also? Because some objects have a huge amount of parameters. For example the table has a set of objects on it, position, orientation, visible list (who watches it). When you place it into inventory you loose all those parameters.

Maybe there's a need to create class InventoryItem to store just object type and amount? But how to manage actions in inventory, like fruit eating? So it can be done using some object-specified factory. So you have Apple - a 3D object and static AppleManager that has functions create3DApple(), inventoryUse() etc. Too much static ones are used to be a bad practice - minimize singletons is told everywhere.

Next idea is to make AppleInventoryItem - funcional object for inventory. But there's some user-defined data for this type of "Parameter-change objects" which must be static too, or you'll need something like ParamObjectData* data = (ParamObjectData*) getWorld()->getObjectData(PARAM_OBJECT, OBJECT_APPLE); because getObjectData returns all the data for all types of objects, that's the bad practice too.

Share this post


Link to post
Share on other sites
Advertisement
So your question is really two-fold:

1) How to manage the difference between objects in world (where they are networked) and objects in inventory (where only the holder knows about the object, and you really don't need the additional parameters).

2) How to apply all the parameters of an object when it's used out of inventory, where it's only stored as type/count.

I would recommend that you separate "object types" versus "object instances." You can load all the object types from script when the server/game starts, because there will only be a limited number of object types. Each type has an ID.

Then you have a "generic object object" which contains an object type, and does the networking necessary (who can see it, pick it up, etc). This is configured with the object type ID. To render the object, you get the position/orientation from the networked generic object, but look up the model/sprite/texture/particle-effects/whatever from the object type by ID.

When the object is picked up, you destroy the in-world generic object, and instead increment the inventory for the object type ID in question for the picker-upper.

Finally, when an object is used out of inventory, you decrement the count from inventory, and apply the object actions as specified by the object type, not the object instance!

This means that you separate different classes of data into different containers, because the classes of data are operated on differently. Presentation and effects go in the object type; networking goes into the networked-object object, and inventory goes into the inventory-count object.

Share this post


Link to post
Share on other sites
Ok, I'll try to figure out with code. Because your object type is equal (as I understood) to my object factory.


class AbstractType
{
// When user wants to drop an item
AbstractObject* create(Point pos);
char* name;
};

class AbstractObject
{
int m_subtype;
Point pos;
std::list<AbstractObject*> knownList;

void onClick(Player p) {}
};

// It's a box where you click and sell-buy some items
class MarketType : public AbstractType
{
// Every market has some list of possible items to sell/buy
// It's just a list for owner to select what to sell and to buy
std::list<ItemType> canSellBuy;
};

// It's a real market on the ground
class MarketInstance : public AbstractObject
{
// This is a list of things that market sells/buys with some count, price
std::list<MarketItem> marketList;
};


So the user can create many market types, ancient, modern, what he likes. And there's a need to create files like:

Market0.txt:
name=Ancient market
can_sell_buy=APPLE,MANGO,WOOD

Market1.txt:
name=Modern market
can_sell_buy=BREAD,BUTTER,PISTOL

So there's need no difficulty for users to create new market that they like.

First problem is how to link those object types and instances, because when user want to add something, there must be something like:


void MarketInstance::doAddItemToMarket(ItemType item, int price)
{
ObjectType* type = getWorld()->getObjectType(TYPE_MARKET, m_subtype);
if(((MarketType*) type)->canSellBuy(item))
{
Do_something();
}
else
{
alert("This market doesn't support this thing.");
}
}

Share this post


Link to post
Share on other sites
Maybe we need to add after AbstractObject* create(Point pos); the function void use(int amount) to solve the problem of eating fruits, using medicine etc? Or I'm wrong?

Also the question is to organize unique objects like post cards, images, texts. Because the addition of new market or new fruit is config-based feature, it can be updated after game restart or even smarter, don't care. But images/photos are stored in database, and I have no idea to implement it as a part of ObjectType/ObjectInstance ideology.

Share this post


Link to post
Share on other sites
In a generic model, there would be only one single type, crudely defined as:
std::map<std::string, void *> object;

map is the actual instance of an object.
string is the property name.
void * is data specific to property.

And that is it. What the logic does with that is entirely up to user. There is no class hierarchy, no inheritance, polymorphism, etc...

Want to make something a shop - put (is_shopkeeper,true) and (sell_list,[a,b,c]) into the map.

When interacting with an object, one can then query if it has is_shopkeeper property and it's set to true and then display whatever GUI is needed by querying the sell_list and interpreting the parameter as std::list<Item>.

Eventually this would evolve into full-blown key-value store.

But unless closely controlled and regularly tested, such systems can quickly devolve into unmaintainable mess, since they lack any kind of constraints usually imposed by language syntax as well tool support, type safety and dependency checking.

Most of these can be constructed in C++ using various techniques.
A probably much faster way is to use a scripting language, javascript would be ideal due to its prototype-based design, and just define everything in there, no need for separate configuration and such.

This type of design is basically C++'s arch nemesis, the exact opposite of what it's best at, namely painfully static design.

Share this post


Link to post
Share on other sites
Yes, map of properties is a universal thing - different object types have different properties. But those types have different actions. Yes, for those actions we have object instances in 3D world, but what about inventory? Because every INVENTORY_USE_ITEM and other inventory actions should be processed differently. For example:

1. Apple on use adds some parameter(s).
2. Car keys call the car near person.
3. Gun can shoot only some bullets.
4. Stick can be used for near attack.
5. Shields have other actions.
6. Parachute bags can be used when falling.
7. Other bags can be used for reactive fly.

All those actions are inventory actions (you don't need to create 3D object with a list of visible objects, position etc.)

Yes, those actions could be processed simply by IF/ELSE, but who knows if someone will add something new?

Share this post


Link to post
Share on other sites
You can divide your object types to several main categories, e.g. "NPCs", "PCs", "things", "boats" etc. All objects in a category has a lot in common, so avoiding those numerous if's, e.g. objects can be movable or not etc. Consider "things", they are world objects that can be placed in inventory etc. "Usage" can have some effects that can be stored as properties of recommended by hplus0603 "object type". E.g. object type "apple", "usage" will have effects: "energy" "+20" (+ maybe some others). When object instance is used, all its parent "object type" usage properties are enumerated and its effects are applied.

it's a trade-off between more straightforward approaches and more universal ones
advantage: still quite simple and straightforward
disadvantage: still requires some amount of auxiliary code writing

Share this post


Link to post
Share on other sites
You probably also want to look into the concept of interfaces in C++ (generally implemented as abstract base classes).


class IInventoryCategory {
public:
virtual Bitmap *getBitmap() = 0;
virtual char const *getName() = 0;
virtual int getSortOrder() = 0;
};

class IInventoryObject {
public:
virtual IObject *baseObject() = 0;
virtual IInventoryCategory *getCategory() = 0;
virtual Bitmap *getBitmap() = 0;
virtual char const *getName() = 0;
virtual char const *getActivationName() = 0;
virtual bool needsTarget() = 0;
virtual bool consumesOnActivation() = 0;
virtual bool canActivate(IPlayer *player, IObject *target) = 0;
virtual void activate(IPlayer *player, IObject *target) = 0;
};


The Inventory is simply a sorted list of IInventoryObject pointers and counts, displayed/grouped by IInventoryCategory. When displaying, you can display enable/disable state based on the return values from canActivate() for example.

The actual implementation can vary -- a CWeapon doesn't need to be the same implementation as a CConsumable; they just both need to derive from IInventoryObject.

Share this post


Link to post
Share on other sites
As I understand you advise to process all the inventory operations in inventory module according to parameters and type of the object?

Because I thought that objects in game is like some plugins. We have some engine and if we want to add some non-core abilities, it can be added to objects functionality.

Even if I'll will process param-change items, clothes, rocket bags, swords, pistols then once someone will need to add some object-plugin with other functions.

Share this post


Link to post
Share on other sites
Also there is a problem with inventory objects that are represented by one 3D object class, for example, fruits, guns, meditine. Even if they have different actions in inventory, they have everything in common in 3D space. All of them are simply staying on the ground and put themselves in inventory on user click near them.

As I understand, they must have something like:


class InventoryObjectInstance : public AbstractObject
{
public:
void onClick(Player* p)
{
p->getInventory()->addItem(getWorld()->getPlugin(type)->getObjectType(subtype), count);
}
private:
int type, subtype, count;
};


What to do when object is being stored? Do I need to save it as an InventoryObject or instantly Fruit? Because if I save InventoryObject, I need to read is an inventory object, so there must be a plugin/object-type.

Also please help me how to manage with unique objects like images, sounds etc.?

Share this post


Link to post
Share on other sites
The main thing about an Apple is that it's an object of class Apple -- which in turn is an object of class Fruit, with the name "Apple" and the model "apple.x" and the inventory bitmap "apple.png" and the action script "onActivate(self, player): player.health += 10; self.consume()"

All object classes should/could be loaded on game start time, or level load time. Instances of objects then carry only the information that's specific to the instance. Your design has two kinds of instances: instance in inventory (2D), and instance in world (3D). When an object moves from one to the other, you should create the new instance object, and forward the object type (in this case, the pointer-to-Apple) to the new instance.

Share this post


Link to post
Share on other sites
No, apple is not a class. If I did so then every of 5000 object types had own class. But what if we'll group it? Apple, cherry, meat, medicine and other 100 health-energy items have the same class.

Yes, you told right that inventory instance has some transformation to world 3D instance but I talk about hard interactions:

1. Apple, cherry, meat (changing parameters) that have class ParamChangeInventotyItem that have action of parameter change on use, after turn to 3D they have Gettable3DObject class that has only action to get from ground and turn to ParamChangeInventotyItem again.

2. Clothes have some another action on use (so we have class ClothesInventoryItem), but turns to Gettable3DObject.

3. Gun has another action in inventory (class GunInventoryItem), but it also turns to Gettable3DObject.

4. The same is with the sword (class SwordInventoryItem) -> Gettable3DObject.

Did you noticed that every different inventory class has the similar behaviour in 3D space. Than talk about next interactions:

5. Table (has class PutableInventoryItem), it turns to Table3DObject, because in 3D world it has some actions, like put food on it, eat something.

6. Chair (class PutableInventoryItem), turns to another Chair3DObject, because we can sit and stand only.

7. Bench (class PutableInventoryItem), turns to another Bench3DObject

And so on...

Share this post


Link to post
Share on other sites
Quote:
5. Table (has class PutableInventoryItem), it turns to Table3DObject, because in 3D world it has some actions, like put food on it, eat something.

Those are user (inter)actions. They have little to nothing to do with 3D.

The simplest way is to define a base class:
struct Action {
virtual string getName();
virtual int id();
virtual string getDescription();
virtual string perform();
}

struct InteractiveObject { // base class for objects user can interact with
list<Action *> getActions(User *); // get list of actions for given user
};


This approach is known as command pattern. On creation, each instance populates its actions, or creates them on demand in response to getActions. In Java, Swing uses this with Action class to define UIs. Most other UI frameworks offer similar. QT, being almost a clone of Java standard library uses almost identical semantics.

Renderer will be completely separate, the only connection will be some ray picking algorithm, which will determine which piece of geometry the user clicked.

Then, the ID of this geometry is sent to server. Server resolves the ID to object, calls getActions, and serializes that back to client.

Client now displays the UI (popup or similar) with list of action names.

When user chooses an action, it sends its ID or similar to server, and server calls ->perform() on that action.

But there really shouldn't be any, or very little relation between 3D and logic. Or, logic should still be separate, but perhaps connect itself to collision detection. I don't think there is any realistic need for logic to know any kind of details regarding meshes or textures.

At least not at this stage, or for simple to moderately complex interactions.

Share this post


Link to post
Share on other sites
Quote:
Original post by Anton Vatchenko
No, apple is not a class. If I did so then every of 5000 object types had own class. But what if we'll group it?


If you look at my actual suggestion, I used "class" to mean "object type" more than "C++ type." The differentiation between "Apple" and "Orange" is done using properties, not code. (And, perhaps, scripting -- again, see my example).

Scripting is really powerful when you want to describe thousands of slightly different objects. You might even want to generate the scripts from an Excel sheet or database.

Btw: This thread has very little to do with networking -- if it weren't so long, I'd move it to general programming.

Share this post


Link to post
Share on other sites
Antheus, the problem is not in renderer, object picking, and not in getting several actions for 3D object. We're talking about life cycle of an object - from inventory to 3D and back. Some objects are represented by several classes in inventory and same 3D classes, some have same inventory classes and several 3D classes. Engine is not scriptable. 100% C++ and text/binary config files.

And still a problem is to implement unique images, sounds, texts in inventory and their processing. Because every preconfigured object can have a link to object type where you can getName() that gets m_name field or getType that gets m_type field, but unique objects are not stored in engine. They are in database.

Share this post


Link to post
Share on other sites
Well, what you're talking about here is the result of crossing the list of verbs with the list of nouns.

The trick here is spotting that you're trying to express a MANY<->MANY relationship between nouns and verbs.

One of the simple ways of doing this is to imagine a grid. It has verbs across the top, and nouns down the side. The intersection of the rows and columns are what happens when you try and do verb V to noun N.

At each intersection, you want to be able to set what happens. So you have "EAT/BOOK" which says "it's not nutritious", "EAT/APPLE" which increases a stat. "READ/APPLE" doesn't do anything and "READ/BOOK" tells you what the book is about.

So what you have here is a "sparse grid" of actions. Where there's no interaction, you can just omit it. Your verbing system detects there's nothing there and says "don't be silly". Otherwise it calls the verb function.


So, the next question is how do you populate your rows?

The easiest way to do this is to have "mixin" classes. These are small aspects of objects, "Eatable", "Readable" etc.

The mixins just populate the rows of the object with function pointers.

This makes it really easy to do things like create new classes of objects; you can do it in one of two ways. Firstly you can list the mixins which make it up, but also you can use another object as a prototype.

So you can pick an INSTANCE of an object and use it as the root of a new class. You can say that "Orange001 is like Apple887, except that it has attribute[colour]=orange".

You can also clone the objects and add a mixin. Oranges often have a small label on them saying where they come from, so you can say "Orange001 is Readable, text='grown in Florida' " and now it can be read.

This is particularly useful for highly dynamic environments when you don't know how many of the things to make.

Serialising the object to a DB or other store is a little more tricky but with some thought upfront, not so bad. Firstly it's a set of name=value pairs. And secondly it's just a set of verb=functionname pairs. Store both and you can put the object in and out of a datastore.

Share this post


Link to post
Share on other sites
No-no-no. I'm not talking about mixing of actions and objects. I don't make them eatable, readable etc. I group them to executeable. Every item is executable and dropable. Problems are not in in those actions.

First problem is to process the use of unique objects and non-unique in good way.

Share this post


Link to post
Share on other sites
I already suggested how to do this. You create an object instance that represents the common properties of an object "type." This would describe what is an "Apple" and what is an "Orange." You then create a separate object which contains the unique properties, such as 3D position or enchantment state (if objects can be enchanted). That object references the type object for all common properties.

Share this post


Link to post
Share on other sites
Please describe how to do with database interactions and unique objects, because as I understand we store in inventory table something like:

user, type, subtype, count:
1, 2, 1, 5
1, 7, 3, 2
1, 3, 2, 5
2, 1, 5, 7

and then:

// Some typedef that adds to global type map
ADD_TYPE(2, GeneralGun);
ADD_TYPE(7, ParamChanger);
ADD_TYPE(3, Image);
ADD_TYPE(1, Sound);

void Player::processRow(int type, int subtype, int count)
{
addToInventory(getWorld()->typeList[type]->create(subtype), count);
}

Item* GeneralGun::create(int subtype)
{
return m_subTypeList->get(subtype)->create();
}

Item* Image::create(int subtype)
{
return new ImageItem(query_result("select * from images where id=" + subtype));
}
...

Share this post


Link to post
Share on other sites
You still haven't described what's what. You need more than one table.

The "class" of an object, or the "prototype" of an object in certain systems, lives in the "object class" table.

The "instance" of an object, which lives in the world, lives in an "instances" table.

The "instances" table contains information such as position, and contains a reference to the appropriate class in the "class" table.

The "class" table contains all the information that never changes about an object type.

You mirror rows in these tables into objects in your game.

When an object changes state -- from "in world" to "in inventory" for example -- then you change objects (delete the in-world object, increment the count for the object class in question in the inventory of the player) and also change the database to match (update the row that counts number-of-instances-of-this-type-for-this-player, and delete the row that says instance-of-object-type-in-world).

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement