Table of Contents
Items are things that players and other entities can possess, such as guns, swords, loot, etc.
Since these are, most of the time, attached to other entities and do not have a position of their own in the world, they do not need to be BigWorld entities. The only exceptions are items lying on the ground. For more details, see Dropping and picking items up.
The simplest way of representing an item is as an integer value, representing the item type ID. This type ID can then be transmitted between the server and clients.
It is recommended that an alias be defined for
the item type in the file
(where <res>
/scripts/entity_defs/alias.xml<res>
is the
first folder specified in environment variables
BW_RES_PATH
). In the sample implementation, the definition
looks like this:
<root> ... <ITEMTYPE> INT32 </ITEMTYPE> ....
The items types are enumerated inside the file
:
<res>
/scripts/common/ItemBase.py
class ItemBase( object ): ... NONE_TYPE = -1 STAFF_TYPE = 2 STAFF_TYPE_2 = 3 DRUMSTICK_TYPE = 4 SPIDER_LEG_TYPE = 5 BINOCULARS_TYPE = 6 SWORD_TYPE = 7 SWORD_TYPE_2 = 9 GOBLET_TYPE = 17 ...
Mapping from item type ID to specific item look
and behaviour can be coded on the client and server as needed. This can be
done by simply using lookup tables or item classes. In the sample
implementation, item classes are used on the client to implement the
behaviour of each type of item. All items types derive from a base
Item
class and must specialise items behaviours
like enact
, enactIdle
,
enactDrawn
and
use
.
-
In
:<res>
/scripts/client/Item.pyimport ItemBase .... class Item( ItemBase.ItemBase ): def use( self, user, target ): ... def name( self ): ... class Food( Item ): def use( self, user, target ): .... user.eat( self.itemType ) user.cell.eat( self.itemType ) ...
The type classes are also used to carry information about the items look, like models and icons.
-
In
:<res>
/scripts/client/Item.pyclass Food( Item ): ... modelNames = { STRIFF_DRUMSTICK: "sets/items/item_food_drumstick.model", SPIDER_LEG: "characters/npc/spider/spider_leg.model", WINE_GOBLET: "sets/items/grail.model" } ... guiIconNames = { STRIFF_DRUMSTICK: "gui/maps/icon_items/icon_food_drumstick.tga", SPIDER_LEG: "gui/maps/icon_items/icon_spider_leg.tga", WINE_GOBLET: "gui/maps/icon_items/icon_grail.tga" } ...
Within this system, creating a new item type is done by inserting a new item type ID into the items type list and implementing class for the specialised behaviour.
You can build more complex structures to represent an item as needed, such as adding its ammo count. This information could be encoded into the INT32 data type, or a new class structure could be created to implement it.
The creation and deletion of items are best done using a global items manager on the base. This manager should ensure that items are created and destroyed according to specific rules, assign unique serial number to items, and enforce security so that things such as duplication exploits cannot be used.
For the item currently equipped by an Avatar to be properly rendered on all clients, the property containing the item type ID must be current on all clients at all times. Whenever a player chooses to equip a new item, it must notify its cell. The cell will then update all other clients with the newly selected item type ID.
In the sample implementation, the property
rightHand
carries the Avatar’s currently equipped item.
The fact that this property is flagged
OTHER_CLIENTS[1] means that the owner Avatar
in the
client will not be updated when the property changes in the cell as the
changes to the property have been initiated by the client
Avatar
.
-
In
:<res>
/scripts/entity_defs/interfaces/Avatar.def<root> ... <Properties> <rightHand> <Type> ITEMTYPE </Type> <Flags> OTHER_CLIENTS </Flags> <Default> -1 </Default> <Editable> true </Editable> </rightHand> ...
-
In
<res>
/scripts/client/Avatar.py:class PlayerAvatar( Avatar ): ... def equip( self, itemType ... ): ... self.rightHand = itemType # change locally self.cell.setRightHand( itemType ) # tell the world ...
-
In
<res>
/scripts/cell/Avatar.py:def setRightHand( self, sourceID, itemType ): ... self.rightHand = itemType # will propagate to all otherClients
To render the item in the player's hand, the 3D model representing the item must be attached to a hard point in the Avatar model. This should be done whenever the player equips a new item. The sample implementation uses the set_rightHand method to do that. The set_rightHand method is implicitly called whenever the value of the property rightHand is updated in the client by the server.
-
In
:<res>
/scripts/client/Avatar.pyclass Avatar( BigWorld.Entity ): def set_rightHand( self, oldRH = None ... ): ... self.lockRightHandModel( True ) ... def lockRightHandModel( self, lock, itemLoader = None ): ... self.rightHandItem = Item.newItem( self.rightHand ) ... if self.rightHandItem != None: ... self.model.right_hand = self.rightHandItem.model ... ...
An inventory is a collection of items. Inventories are typically stored on both the client and the base. Having the inventory on the cell would increase the load of migrating entities from cell to cell as they move in the space.
The authoritative copy of an inventory should always reside on the base entity, where it can be easily stored and retrieved from persistent storage through the BigWorld database interface. The client copy of the inventory can then be regenerated from the base whenever necessary.
The client should have a graphical user interface to the inventory, so that players can view their items. Players can use this interface to add or remove items from their inventories. This should then be updated on the base entity and stored on the database at convenient times.
In the sample implementation, the inventory data is stored in a set
of BASE_AND_CLIENT properties in the
Avatar
entity:
-
inventoryItems
-
inventoryLocks
-
inventoryGoldPieces
inventoryItems
is an array of the type
InventoryEntry. inventoryLocks
is an
array of the type LockedEntry. Both
InventoryEntry and LockedEntry types
are defined in the
. These types are defined using BigWorld's
FIXED_DICT type feature.
<res>
/scripts/entity_defs/alias.xml
fileinventoryGoldPieces
is of type
INT32.
-
In
:<res>
/scripts/entity_defs/Avatar.def<root> <Properties> ... <inventoryItems> <Type> ARRAY <of> InventoryEntry </of> </Type> <Flags BASE_AND_CLIENT </Flags> <Persistent> true </Persistent> <Default> ... </ Default > </inventoryItems> <inventoryLocks> <Type> ARRAY <of> LockedEntry </of> </Type> <Flags> BASE_AND_CLIENT </Flags> <Persistent> true </Persistent> </inventoryLocks> <inventoryGoldPieces> <Type> INT32 </Type> <Flags> BASE_AND_CLIENT </Flags> <Persistent> true </Persistent> <Default> 100 </Default> </inventoryGoldPieces> ....
-
In
:<res>
/scripts/entity_defs/alias.xml<root> ... <ITEMSERIAL> INT32 </ITEMSERIAL> <LOCKHANDLE> INT32 </LOCKHANDLE> <GOLDPIECES> INT16 </GOLDPIECES> <InventoryEntry> FIXED_DICT <Properties> <itemType> <Type> ITEMTYPE </Type> </itemType> <serial> <Type> ITEMSERIAL </Type> </serial> <lockHandle> <Type> LOCKHANDLE </Type> </lockHandle> </Properties> </InventoryEntry> <LockedEntry> FIXED_DICT <Properties> <lockHandle> <Type> LOCKHANDLE </Type> </lockHandle> <goldPieces> <Type> GOLDPIECES </Type> </goldPieces> </Properties> </LockedEntry> ....
The fact that the inventory properties are flagged BASE_AND_CLIENT, means that:
-
When initialised, the owning client entity will be carrying an exact copy of the properties as set on the base entity
-
The inventory property will not exist either in the cell entity or in client Avatars not controlled by the owning player.
-
Changes to the inventory property's value in the base will not be propagated to the client. The game logic must take care of keeping both copies synchronised as changes are made to the inventory.
One example of such logic is an item being added to the inventory after being picked up by the player:
-
After the pick up, the base adds the item into the inventory and notifies the cell.
-
The cell forwards the notification to the client.
-
In
:<res>
/scripts/base/Avatar.pydef pickUpResponse( self, success, droppedItemID, itemType ): ... itemsSerial = self.inventoryMgr.addItem( itemType ) self.cell.pickUpResponse( True, droppedItemID, itemType, itemsSerial ) ...
-
In
:<res>
/scripts/cell/Avatar.pydef pickUpResponse( self, success, droppedItemID, itemType, itemSerial ): ... self.client.pickUpResponse( True, droppedItemID, itemSerial ) ...
-
-
In the client, the Player replicates the addition of the item into the inventory.
-
In
<res>
/scripts/client/Avatar.py:class PlayerAvatar( Avatar ): ... def pickUpResponse( self, success, droppedItemID, itemSerial ): ... droppedItem = BigWorld.entities[ droppedItemID ] self._pickUpProcedure( droppedItem, itemSerial ) ... def _pickUpProcedure( self, droppedItem, itemSerial ): ... itemType = droppedItem.classType self.inventoryMgr.addItem( itemType, itemSerial ) ...
-
-
In the sample implementation, the class
InventoryMgr encapsulates the logic of adding,
removing, selecting, locking, and trading items in the inventory. The
InventoryMgr is initialised with a reference to the
inventory holder entity. It looks for the inventory properties
(inventoryItems
, inventoryLocks
and inventoryGoldPieces
) in that entity.
Only changes made to the entity's properties (on the base) will be
written to the database. InventoryMgr member
variables will not persist, and should be treated as temporary. One such
variable is _currentItemIndex
, which is not
considered a persistent property and is initialised to
NOITEM (-1) every time a new
inventory is instantiated.
Adding and removing items to/from the inventory is just a matter
of appending and popping items into/from the
inventoryItems
array, taking care of respecting
locking rules, if any, and internal consistency constraints, like
resetting the selected item, if it is the one being removed from
inventory.
-
In
:<res>
/scripts/common/Inventory.pyNOLOCK = -1 NOITEM = -1 ... def __init__( self, entity ): self._entity = weakref.proxy( entity ) self._curItemIndex = NOITEM ... def addItem( self, itemType ... ): ... if itemSerial is None: itemSerial = self._genItemSerial() entry = { "itemType": itemType, "serial": itemSerial, "lockHandle": NOLOCK } self._entity.inventoryItems.append( entry ) ... return itemSerial def removeItem( self, itemSerial ): index = self._itemSerial2Index( itemSerial ) # throws is serial not found entry = self._retrieveIfNotLocked( index ) # throws if item is locked try: item = inventory[ itemIndex ] inventory.pop( itemIndex ) except IndexError: errorMsg = 'removeItem: invalid item index (idx=%d)' raise IndexError, errorMsg % itemIndex if self._curItemIndex == index: self._curItemIndex = -1 elif self._curItemIndex > index: self._curItemIndex -= 1 ... return entry[ "itemType" ]
Note
Instead of a straight reference, a weakref to the entity is kept to avoid creating a cyclic reference that will prevent the entity from being eventually deleted.
Note that in the sample implementation items are not referenced by their index inside the inventoryItems array. Instead, they are assigned a serial number when added to the inventory, and are referenced by that serial throughout all their life inside it.
The reason for this is to allow simultaneously changes to the inventory from multiple sources (e.g., game client, web interface, mobile device). In this case, if direct indices are used, references to items can become obsolete while a request is still being processed.
In the example, serial numbers are not attached to the item itself, but to their existence in an inventory, that is, serial numbers are guaranteed to be unique only within a single inventory. Two inventories can contain items with serial numbers duplicated between them.
A more comprehensive items systems can use a global serial number generator and have them assigned to items when they are first created. This serial numbers can then be used throughout all game subsystems to uniquely refer to items, to track duplicated items, etc.
When using serial numbers it is important to have a single authoritative copy of the inventory generating the serials for each item, otherwise serial numbers can become inconsistent between instances of the same inventory. When adding an item to the non-authoritative copy of the inventory, its assigned serial number must be provided along with the item.
-
In
<res>
/scripts/common/Inventory.py:def addItem( self, itemType, itemSerial = None ): ... entry = { "itemType": itemType, "serial": itemSerial, "lockHandle": NOLOCK } self._entity.inventoryItems.append( entry ) ... return itemSerial
As mentioned above, the items can usually be represented as a property of an entity (or an entry into an array property). However, there is one case where this is not sufficient, and the item needs to be an entity.
This is the case of an item that has been dropped on the ground. The reason for this is that if you walk away from an item that you dropped, other people still need to see it.
If the item was just a property of a player, then when the player left the area of interest (which is usually about 500m), the property would disappear as well, and so would the dropped item.
On the other hand, if the dropped item itself is an entity, then other players will see it, as long as it is in their area of interest.
Making a dropped item an entity means that you can write interaction scripts just like any other entity in the world. You can target it, shoot it, pick it up, etc...
The process of dropping and picking items up should be something similar to the steps described below.
Initially, the item is in the player’s inventory, expressed as an entry in the inventoryItems property, the itemType field in the entry holds the item type ID.
A game would follow the steps described below for an item drop:
-
Player uses the user interface to drop an item.
-
Because the item may be unavailable in the server (e.g., in case it has just been locked in the inventory from a web interface to the trading system), the client Avatar makes the drop item requests to the base. If the player can drop the item, the base removes the item from the inventory and notifies the cell about the dropped item:
-
In
<res>
/scripts/client/Avatar.py:class PlayerAvatar( Avatar ): ... def dropOnline( self ): ... self.base.dropRequest( itemSerial )
-
In
<res>
/scripts/base/Avatar.py:def dropRequest( self, itemSerial ): ... itemType = self.inventoryMgr.removeItem( itemIndex ) self.cell.dropNotify( True, itemType ) ...
-
-
The cell creates a DroppedItem entity that matches the item dropped by the player:
-
In
<res>
/scripts/cell/Avatar.py:def dropNotify( self, success, itemType ): ... BigWorld.createEntity( "DroppedItem", ... ) ... self.rightHand = ItemBase.ItemBase.NONE_TYPE
-
-
The server informs all clients within the area of interest that a DroppedItem has been created (via Python's function BigWorld.createEntity).
-
The confirmation to the Avatar for his drop request comes from the DroppedItem itself. The client then plays the drop animation and removes the item from player’s right hand (for simplicity, the code that synchronises the drop animations, the item model being removed from the avatar’s right hand and reappearing on the ground is not shown here):
-
In
<res>
/scripts/client/DroppedItem.py:def enterWorld( self ): ... dropper = BigWorld.entities[ self.dropperID ] dropper.dropNotify( self ) ...
-
In
<res>
/scripts/client/Avatar.py:class Avatar( BigWorld.Entity ): ... def dropNotify( self, droppedItem ): ... self._dropProcedure( droppedItem ) def _dropProcedure( self, droppedItem ): ... droppedItem.dropComplete() ...
-
-
All clients now draw the correct model for the DroppedItem entity on the ground:
-
In
<res>
/scripts/client/DroppedItem.py:class DroppedItem( BigWorld.Entity ): ... def dropComplete( self ): ... self._showModel() def _showModel ( self ): ... self.model = self.item.model
-
Initially, the item is a DroppedItem entity lying on the ground, as described in Dropping an item on the ground.
A game would follow the steps described below for picking up an item:
-
Client requests to pick item up.
-
In
<res>
/scripts/client/Avatar.py:class PlayerAvatar( Avatar ): ... def pickExecute( self, droppedItem ): ... self.cell.pickUpRequest( droppedItem.id ) ...
-
In
<res>
/scripts/cell/Avatar.py:def pickUpRequest( self, sourceID, droppedItemID ): ... item = BigWorld.entities[ droppedItemID ] item.pickUpRequest( self.id ) ...
-
-
The item locks itself as being picked up by the requesting Avatar. Further requests for pickup will be denied by the server. The item also notifies the requester base about the pickup so that the item is added to the player inventory. Finally, the item sets a timer to remove itself from the world.
-
In
<res>
/scripts/cell/DroppedItem.py:def pickUpRequest( self, whomID ): ... if self.pickerID == 0: picker = BigWorld.entities[ whomID ] picker.base.pickUpResponse( True, self.id, self.classType ) self.addTimer( 5, 0, DroppedItem.DESTROY_TIMER ) self.pickerID = whomID ...
-
In
<res>
/scripts/base/Avatar.py:def pickUpResponse( self, success, droppedItemID, itemType ): if success: itemsSerial = self.inventoryMgr.addItem( itemType ) self.cell.pickUpResponse( True, droppedItemID, itemType, itemsSerial ) ...
-
-
The cell notifies all clients about the pick up and updates the avatar’s right hand property.
-
In
<res>
/scripts/cell/Avatar.py:def pickUpResponse( self, success, droppedItemID, itemType, itemSerial ): if success: # sucess:notify all clients this entity base self.client.pickUpResponse( True, droppedItemID, itemSerial ) self.otherClients.pickUpNotify( droppedItemID ) self.rightHand = itemType ...
-
-
All clients are notified that the Avatar is picking up the item. A picking item up animation is started on the Avatar’s model. Note that, although other clients will see the item model in the player hands as a consequence of rightHand being set in the server, the PlayerAvatar will not have its rightHand property updated and must explicitly equip the item (remember that rightHand is flagged OTHER_CLIENTS).
-
In
<res>
/scripts/client/Avatar.py:class Avatar( BigWorld.Entity ): ... def pickUpNotify( self, droppedItemID ): ... droppedItem = BigWorld.entities[ droppedItemID ] self._pickUpProcedure( droppedItem ) ... class PlayerAvatar( Avatar ): ... def pickUpResponse( self, success, droppedItemID, itemSerial ): ... droppedItem = BigWorld.entities[ droppedItemId ] self._pickUpProcedure( droppedItem, itemSerial ) ...
-
-
The Player adds the picked item into his inventory and selects it.
-
In
<res>
/scripts/client/Avatar.py:class PlayerAvatar( Avatar ): ... def pickUpFinish( self, droppedItem ): ... itemType = droppedItem.classType self.inventoryMgr.addItem( itemType, itemSerial ) self.inventoryMgr.selectItem( itemSerial ) ...
-
-
On the base, the timer goes out and the DroppedItem entity destroys itself.
-
In
<res>
/scripts/cell/DroppedItem.py:def pickUpRequest( self, whomID ): ... self.addTimer( 5, 0, DroppedItem.DESTROY_TIMER ) def onTimer( self, timerId, userId ): ... if ( userId == DroppedItem.DESTROY_TIMER ): self.destroy()
-
-
The item has been added to the player inventory in both the client and the server. All clients draw the item in player’s right hand. The dropped item has been removed from the server.
If the game design calls for some sort of asynchronous transactions to be carried out with inventory items, it will be necessary to implement locking of items inside the inventory.
During asynchronous transactions, like those taking place over a mobile device or web interface, a player may offer one or more items for trading. A second player may, at his own time, inspect the items being offered and in turn reply the offer with one or more of his items. When the first player inspects and accepts the items in the reply offer, the transaction can be carried out.
Note
Although this is not a requirement for asynchronous transactions, locking also offers a way to reference a set of items (and maybe also some amount of currency) using a convenient single handle.
From first offer to final acceptance, a finite amount of time will have passed, from a couple of seconds to several hours or even days. Problems will occur if the items initially offered are no longer available when both parties accept the transaction (if, for example, one of the players drops, consumes or trades with a third player any of the offered items).
One approach to avoid the problem is to lock the items inside the inventory once they have been committed to a trade offer. Locked items should not be available for dropping, offering on a second trade nor equipping (thus consuming). The player should be free to unlock the items at any time. Unlocking an item should invalidate the trade associated with it.
In the sample implementation, items are flagged as locked by assigning a valid lock handle to the lockHandle field in the item’s entry in the inventoryItems array. Some amount of gold pieces can also be locked together with the set of items. The gold information is stored in the inventoryLocks array. Locking items and gold pieces yields a lock handle. This lock handle can than be used to reference the locked lot, either to unlock it or to trade it for gold, other items or a combination of both.
-
In
<res>
/scripts/common/Inventory.py:NOLOCK = -1 ... def itemsLock( self, itemsSerials, goldPieces ): lockHandle = self._getNextLockHandle() self.itemsRelock( lockHandle, itemsSerials, goldPieces ) return lockHandle def itemsRelock( self, lockHandle, itemsSerials, goldPieces ): ... itemsIndexes = [] for serial in itemsSerials: index = self._itemSerial2Index( serial ) if self._entity.inventoryItems[ index ][ "lockHandle" ] != NOLOCK: errorMsg = 'Item item already locked (idx=%d)' raise LockError, errorMsg % index itemsIndexes.append( index ) for index in itemsIndexes: self._entity.inventoryItems[ index ][ "lockHandle" ] = lockHandle lockedEntry = { "lockHandle": lockHandle, "goldPieces": goldPieces } self._entity.inventoryLockedItems.append( lockedEntry ) ... def itemsUnlock( self, lockHandle ): index = self._getLockedItemsIndex( lockHandle ) lockedEntry = self._entity.inventoryLockedItems[ index ] self._entity.inventoryLockedItems.pop( index ) for entry in self._entity.inventoryItems: if entry[ "lockHandle" ] == lockHandle: entry[ "lockHandle" ] = NOLOCK
[1] For more information on the OTHER_CLIENTS property distribution flag please refer to the Server Programming Guide chapter Properties.