Game Development Community

dev|Pro Game Development Curriculum

Inventory Manager Tutorial

by Tim Newell · 01/10/2002 (8:34 am) · 62 comments

Download Code File

Inventory Manager v1.0

This is an Inventory Manager that is written in C++, contained in the Player class and accessed and manipulated through scripts. This tutorial covers adding it to your engine and modifying the scripts to use it instead of the scripted inventory system.

***UPDATED***
Updated already?
yep, seems that I can't post a tutorial without finding a flaw in it.

I have the tutorial updated to store the type of item also since it is something useful and is needed in the inventory v3. Most of the changes took place in Inventory.cc and Inventory.h but there were a couple in player.cc. The only script change will be in game.cs.

NEW: After peeking around the string functions in the engine, I realized that Torque had definitions for the functions I was using (plus some other useful functions) and it would take into account multiple compilers and multiplatforms. So I edited the Inventory.cc and Inventory.h file to use Torque defined functions and var types. Download the new zip file if you already have this tutorial working. (you don't have to change anything just replace your old Inventory.cc and Inventory.h files with the new ones and recompile)

***END UPDATED***

C++ Changes

Download the Zip from the link on this tutorial, it contains Inventory.cc and Inventory.h. They both need to be placed into /engine/game and added to your workspace.

Next open /engine/game/player.h and go down to the very end of the file.
After void unpackUpdate(NetConnection *, BitStream *stream); and before }; #endif add:

//Inventory Object
Inventory inv;

also you need to add

#include "game/Inventory.h"

at the top of player.h


Now open /engine/game/player.cc.

Go down to static bool cCheckDismountPoint(SimObject *ptr, S32, const char **argv) (line 3643 in my build) go after it and add these functions:

///////////////////////////////////////////////////////////////////
//Inventory Console Functions
///////////////////////////////////////////////////////////////////
static void cAddInvItem(SimObject *ptr, S32 argc, const char **argv) {
	
	Player *obj = static_cast<Player*>(ptr);
     
    obj->inv.AddItem((char*)argv[2], (char*)argv[3], (char*)argv[4], (char*)argv[5],(char*)argv[6]);
    
} 

////////////////////////////////////////////////////////////////////
static void cRemoveInvItem(SimObject *ptr, S32 argc, const char **argv) {
	
	Player *obj = static_cast<Player*>(ptr);
     
    obj->inv.RemoveItem((char*)argv[2]);
    
}

////////////////////////////////////////////////////////////////////
static const char* cGetInvItem(SimObject *ptr, S32 argc, const char **argv) {
	
	char *returnBuffer = Con::getReturnBuffer(256);
	Player *obj = static_cast<Player*>(ptr);
    dSprintf(returnBuffer,256,"");
	obj->inv.GetItem((char*)argv[2], (char*)argv[3], returnBuffer);
	return returnBuffer;
    
}

////////////////////////////////////////////////////////////////////
static bool cMoveCurPtr(SimObject *ptr, S32 argc, const char **argv) {
	
	Player *obj = static_cast<Player*>(ptr);

	return obj->inv.MoveCurrent((char*)argv[2]);
    
}

////////////////////////////////////////////////////////////////////
static bool cIncInvItem(SimObject *ptr, S32 argc, const char **argv) {
	
	Player *obj = static_cast<Player*>(ptr);

	return obj->inv.IncAmount((char*)argv[2],(char*)argv[3]);
    
}

////////////////////////////////////////////////////////////////////
static bool cDecInvItem(SimObject *ptr, S32 argc, const char **argv) {
	
	Player *obj = static_cast<Player*>(ptr);

	return obj->inv.DecAmount((char*)argv[2],(char*)argv[3]);
    
}

////////////////////////////////////////////////////////////////////
static bool cSetInvItem(SimObject *ptr, S32 argc, const char **argv) {
	
	Player *obj = static_cast<Player*>(ptr);

	return obj->inv.SetAmount((char*)argv[2],(char*)argv[3]);
    
}

////////////////////////////////////////////////////////////////////
static void cClearInv(SimObject *ptr, S32, const char **) {
	
	Player *obj = static_cast<Player*>(ptr);

	obj->inv.ClearInventory();
    
}

Next Scroll down further to void Player::consoleInit() and add the following lines after the Con::addCommand("Player", "checkDismountPoint", cCheckDismountPoint, "obj.checkDismountPoint(\"x y z\", \"x y z\")", 4, 4); line:

//Inventory Console Commands
   Con::addCommand("Player", "addInvItem", cAddInvItem, "obj.addInvItem(name,amount,type,description,max)", 7, 7);
   Con::addCommand("Player", "removeInvItem", cRemoveInvItem, "obj.removeInvItem(name)", 3, 3);
   Con::addCommand("Player", "getInvItem", cGetInvItem, "obj.getInvItem(type,rettype)", 4, 4);
   Con::addCommand("Player", "moveCurPtr", cMoveCurPtr, "obj.moveCurPtr(move)", 3, 3);
   Con::addCommand("Player", "incInvItem", cIncInvItem, "obj.incInvItem(name,amount)", 4, 4);
   Con::addCommand("Player", "decInvItem", cDecInvItem, "obj.decInvItem(name,amount)", 4, 4);
   Con::addCommand("Player", "setInvItem", cSetInvItem, "obj.setInvItem(name,amount)", 4, 4);
   Con::addCommand("Player", "clearInv", cClearInv, "obj.clearInv()", 2, 2);

Now compile torque and get ready to change out the inventory system in the scripts. There is a lot of changes that are made so if a change(s) has been made in a function, I am going to just post the entire function.


Script Changes

All of the script changes will be scripts in this dir: example\fps\server\scripts

First open game.cs

in the function GameConnection::createPlayer(%this, %spawnPoint) after the line:

%player.setShapeName(%this.name);

Add the following code:

//Init Inventory to all Inventory-able objects
   %player.addInvItem("Rifle", "0","weapon","This is a strange blue rifle.",1);
   %player.addInvItem("Crossbow", "0","weapon","This is a strange blue crossbow.",1);
   %player.addInvItem("RifleAmmo", "0","ammo","This is ammo for a rifle.",100);
   %player.addInvItem("CrossbowAmmo", "0","ammo","This is ammo for a crossbow.",50);
   %player.addInvItem("HealthKit", "0","healthKit","This heals you up real nice.",1);

Next Open Item.cs and replace your functions with the functions in the following code:

function ItemData::onThrow(%this,%user,%amount)
{
   // Remove the object from the inventory
   if (%amount $= "")
      %amount = 1;

   if (%this.maxInventory !$= "")
      if (%amount > %this.maxInventory)
         %amount = %this.maxInventory;
   if (!%amount)
      return 0;
   
   if (%user.decInvItem(%this.getName(),%amount) == 0) 
       return 0;

   %this.onInventory(%user,%user.getInvItem(%this.getName(),"amount"));

   // Construct the actual object in the world, and add it to 
   // the mission group so it's cleaned up when the mission is
   // done.  The object is given a random z rotation.
   %obj = new Item() {
      datablock = %this;
      rotation = "0 0 1 " @ (getRandom() * 360);
      count = %amount;
   };
   MissionGroup.add(%obj);
   %obj.schedulePop();
   return %obj;
}

function ItemData::onPickup(%this,%obj,%user,%amount)
{
   // Add it to the inventory, this currently ignores the request
   // amount, you get what you get.  If the object doesn't have
   // a count or the datablock doesn't have maxIventory set, the
   // object cannot be picked up.
   %count = %obj.count;
   
   if (%count $= "")
      if (%this.maxInventory !$= "") {
         if (!(%count = %this.maxInventory))
            return;
      }
      else
         %count = 1;
   
   if (%user.incInvItem(%this.getName(),%count) == 0)
       return 0;
   %this.onInventory(%user,%user.getInvItem(%this.getName(),"amount"));
   
   // Inform the client what they got.
   if (%user.client)
      messageClient(%user.client, 'MsgItemPickup', '\c0You picked up %1', %this.pickupName);

   // If the item is a static respawn item, then go ahead and
   // respawn it, otherwise remove it from the world.
   // Anything not taken up by inventory is lost.
   if (%obj.isStatic())
      %obj.respawn();
   else
      %obj.delete();
   return true;
}

Next open player.cs and DELETE the following code:
(The code is located at the end of datablock PlayerData(LightMaleHumanArmor))

// Allowable Inventory Items
               	maxInv[BulletAmmo] = 20;
   		maxInv[HealthKit] = 1;
   		maxInv[RifleAmmo] = 100;
   		maxInv[CrossbowAmmo] = 50;
   		maxInv[Crossbow] = 1;
   		maxInv[Rifle] = 1;

Next Open crossbow.cs and replace your function with the function in the following code:

function CrossbowImage::onFire(%this, %obj, %slot)
{
   %projectile = %this.projectile;

   // Decrement inventory ammo. The image's ammo state is update
   // automatically by the ammo inventory hooks.
   if (%obj.decInvItem(%this.ammo.getName(),1) == 0) 
       return 0;
   %this.ammo.onInventory(%obj,%obj.getInvItem(%this.ammo.getName(),"amount"));

   // Determin initial projectile velocity based on the 
   // gun's muzzle point and the object's current velocity
   %muzzleVector = %obj.getMuzzleVector(%slot);
   %objectVelocity = %obj.getVelocity();
   %muzzleVelocity = VectorAdd(
      VectorScale(%muzzleVector, %projectile.muzzleVelocity),
      VectorScale(%objectVelocity, %projectile.velInheritFactor));

   // Create the projectile object
   %p = new (%this.projectileType)() {
      dataBlock        = %projectile;
      initialVelocity  = %muzzleVelocity;
      initialPosition  = %obj.getMuzzlePoint(%slot);
      sourceObject     = %obj;
      sourceSlot       = %slot;
      client           = %obj.client;
   };
   MissionCleanup.add(%p);
   return %p;
}

Next Open rifle.cs and replace your function with the function in the following code:

function RifleImage::onFire(%this, %obj, %slot)
{
   %projectile = %this.projectile;

   // Decrement inventory ammo. The image's ammo state is update
   // automatically by the ammo inventory hooks.
   if (%obj.decInvItem(%this.ammo.getName(),1) == 0) 
       return 0;

   %this.ammo.onInventory(%obj,%obj.getInvItem(%this.ammo.getName(),"amount"));
   

   // Determin initial projectile velocity based on the 
   // gun's muzzle point and the object's current velocity
   %muzzleVector = %obj.getMuzzleVector(%slot);
   %objectVelocity = %obj.getVelocity();
   %muzzleVelocity = VectorAdd(
      VectorScale(%muzzleVector, %projectile.muzzleVelocity),
      VectorScale(%objectVelocity, %projectile.velInheritFactor));

   // Create the projectile object
   %p = new (%this.projectileType)() {
      dataBlock        = %projectile;
      initialVelocity  = %muzzleVelocity;
      initialPosition  = %obj.getMuzzlePoint(%slot);
      sourceObject     = %obj;
      sourceSlot       = %slot;
      client           = %obj.client;
   };
   MissionCleanup.add(%p);
   return %p;
}

Next Open health.cs and replace your function with the function in the following code:

function HealthKit::onUse(%this,%user)
{
   // Apply some health to whoever uses it, the health kit is only
   // used if the user is currently damaged.
   if (%user.getDamageLevel() != 0) {
      if (%user.decInvItem(%this.getName(),1) == 0) 
          return 0;
	  %this.onInventory(%user,%user.getInvItem(%this.getName(),"amount"));
      %user.applyRepair(%this.repairAmount);
      if (%user.client)
         messageClient(%user.client, 'MsgHealthKitUsed', '\c2Health Kit Applied');
   }
}

Next Open weapon.cs and replace your functions with the functions in the following code:

function WeaponImage::onMount(%this,%obj,%slot)
{
   // Images assume a false ammo state on load.  We need to
   // set the state according to the current inventory.
   if (%obj.getInvItem(%this.ammo.getName(),"amount") !$= "") {

       %obj.setImageAmmo(%slot,true);

   }

}

function Ammo::onInventory(%this,%obj,%amount)
{
   // The ammo inventory state has changed, we need to update any
   // mounted images using this ammo to reflect the new state.
   // Edited for Ammo Hud
   for (%i = 0; %i < 8; %i++) {
      if ((%image = %obj.getMountedImage(%i)) > 0)
         if (isObject(%image.ammo) && %image.ammo.getId() == %this.getId()) {
            %obj.setImageAmmo(%i,%amount != 0);
		 }
   }
}

Next is Inventory.cs, Im just gonna paste the entire file(except the opening comments) here since a lot of functions were deleted:

//-----------------------------------------------------------------------------
// Inventory server commands
//-----------------------------------------------------------------------------

function serverCmdUse(%client,%data)
{
   %client.getControlObject().use(%data);
}

//-----------------------------------------------------------------------------
// ShapeBase inventory support
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------

function ShapeBase::use(%this,%data)
{
   // Use an object in the inventory.
   if (%this.getInvItem(%data.getName(),"amount") > 0)
      return %data.onUse(%this);
   return false;
}

function ShapeBase::throw(%this,%data,%amount)
{
   // Throw objects from inventory. The onThrow method is
   // responsible for decrementing the inventory.
   if (%this.getInvItem(%data.getName(),"amount") > 0) {
      %obj = %data.onThrow(%this,%amount);
      if (%obj) {
         %this.throwObject(%obj);
         return true;
      }
   }
   return false;
}

function ShapeBase::pickup(%this,%obj,%amount)
{
   // This method is called to pickup an object and add it
   // to the inventory. The datablock onPickup method is actually
   // responsible for doing all the work, including incrementing
   // the inventory.
   %data = %obj.getDatablock();

   // Try and pickup the max if no value was specified
   if (%amount $= "")
      %amount = %this.getInvItem(%data.getName(),"max") - %this.getInvItem(%data.getName(),"amount");

   // The datablock does the work...
   if (%amount < 0)
      %amount = 0;
   if (%amount)
      return %data.onPickup(%obj,%this,%amount);
   return false;
}

//-----------------------------------------------------------------------------

function ShapeBase::throwObject(%this,%obj)
{
   // Throw the given object in the direction the shape is looking.
   // The force value is hardcoded according to the current default
   // object mass and mission gravity (20m/s^2).
   %throwForce = %this.throwForce;
   if (!%throwForce)
      %throwForce = 20;

   // Start with the shape's eye vector...
   %eye = %this.getEyeVector();
   %vec = vectorScale(%eye, %throwForce);

   // Add a vertical component to give the object a better arc
   %verticalForce = %throwForce / 2;
   %dot = vectorDot("0 0 1",%eye);
   if (%dot < 0)
      %dot = -%dot;
   %vec = vectorAdd(%vec,vectorScale("0 0 " @ %verticalForce,1 - %dot));

   // Add the shape's velocity
   %vec = vectorAdd(%vec,%this.getVelocity());

   // Set the object's position and initial velocity
   %pos = getBoxCenter(%this.getWorldBox());
   %obj.setTransform(%pos);
   %obj.applyImpulse(%pos,%vec);

   // Since the object is thrown from the center of the
   // shape, the object needs to avoid colliding with it's
   // thrower.
   %obj.setCollisionTimeout(%this);
}


//-----------------------------------------------------------------------------
// Callback hooks invoked by the inventory system
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
// ShapeBase object callbacks invoked by the inventory system

function ShapeBase::onInventory(%this, %data, %value)
{
   // Invoked on ShapeBase objects whenever their inventory changes
   // for the given datablock.
}


//-----------------------------------------------------------------------------
// ShapeBase datablock callback invoked by the inventory system.

function ShapeBaseData::onUse(%this,%user)
{
   // Invoked when the object uses this datablock, should return
   // true if the item was used.
   return false;
}

function ShapeBaseData::onThrow(%this,%user,%amount)
{
   // Invoked when the object is thrown.  This method should
   // construct and return the actual mission object to be
   // physically thrown.  This method is also responsible for
   // decrementing the user's inventory.
   return 0;
}

function ShapeBaseData::onPickup(%this,%obj,%user,%amount)
{
   // Invoked when the user attempts to pickup this datablock object.
   // The %amount argument is the space in the user's inventory for
   // this type of datablock.  This method is responsible for
   // incrementing the user's inventory is something is addded.
   // Should return true if something was added to the inventory.
   return false;
}

function ShapeBaseData::onInventory(%this,%user,%value)
{
   // Invoked whenever an user's inventory total changes for
   // this datablock.
}


[Scripting Functions of Inventory System]

obj.addInvItem(name,amount,type,description,max); - Adds an item to the System. The Params are the Item's name, amount of the item(should be 0 since player doesnt really have one), the type of item (weapon, ammo, etc), a description of the item (will be used in the Inventory GUI), and the max number of the items the player can hold.

obj.removeInvItem(name); - Removes an Item from the System Note: not from the player just the inventory system and also this is not the same thing as setting it to 0, this removes it totally.

obj.getInvItem(type,rettype); - the "type" variable can be "first", "current", or the specific name of the item. the "rettype" variable can be "name", "amount", "type", "description", "max", or "". (if you specify "" it will return the name, amount, type, and description separated by "\t") If the object cant be found it will return "", if the type is found but not the rettype, then it will return the same thing as "". NOTE: "current" is a floating pointer that can be maipulated with the next function, it defaults at the beginning to equal "first".

obj.moveCurPtr(move); - moves the pointer in the list. move can be "next", "last" (previous item, not last in the list), or "first"; it returns 1 if it moves the pointer and 0 if it is unable to move the pointer.

obj.incInvItem(name,amount); - increments the "name"d items amount by the "amount" specified. returns 1 if it increments and returns 0 if it could not increment (you were already at the max or the item did not exist)

obj.decInvItem(name,amount); - decrements the "name"d item's amount by the "amount" specified. returns 1 if it decrements and returns 0 if it could not decrement (you were already at 0 or the item did not exist)

obj.setInvItem(name,amount); - sets the "name"d item's amount to the "amount" specified. returns 1 if it set the value and returns 0 if it could not. (the item doesnt exist)

obj.clearInv(); - sets all of the item's amounts to 0. This does not delete the items.




That concludes all of the changes to get the inventory manager system to replace the old one. So far that gives no benifits, but when you add a GUI Inventory viewer like my previous Inventory Popup tutorial and Inventory v2 tutorial this system makes it a lot nicer and easier to add. I will have another tutorial soon after this one labeled Inventory v3 tutorial and it will simply be Inventory v2 but it will be using this new inventory system. Stay tuned!

Thanks Phil Carlisle for the suggestions.
Page «Previous 1 2 3 4 Last »
#1
01/10/2002 (6:42 am)
And thank YOU for taking the time to make it. Your contribution to the cause is much appreciated, Tim.

Keep up the good work...

Scott "ILL HANDLE THIS" Schaefer
#2
01/14/2002 (10:37 am)
I can't get the engine to compile, and I know I did everything right...hmm...any ideas what might be causing it?
#3
01/14/2002 (10:42 am)
Nevermind, got it all to work...Great tut dude!
#4
01/18/2002 (8:05 pm)
Hey Tim,
I found a few flaws myself in the engine code you have up. I'll email you my changes and you can post them up here. Hopefully it will help out more people without making them fustrated like I was.

Bryan
#5
01/22/2002 (3:26 am)
I updated Inventory.cc and Inventory.h to use Torque defined string functions and var types since it would take into account multiple compilers and platforms.

Download the new zip file if you already have this tutorial working. (you don't have to change anything just replace your old Inventory.cc and Inventory.h files with the new ones and recompile)

-Tim aka Spock
#6
03/01/2002 (3:04 pm)
Would be great to have container items as well.
#7
06/14/2002 (6:47 am)
Shouldn't

if (%obj.getInvItem(%this.ammo.getName(),"amount") !$= "")

be

if (%obj.getInvItem(%this.ammo.getName(),"amount") !$= "0")

The other way had my weapon false fire when I ran out of ammo then switched to another weapon then back to the weapon with no ammo.

With the 0 my weapon wouldn't false fire when I go back to the weapon with no ammo.


This is the function I am talking about.

function WeaponImage::onMount(%this,%obj,%slot)
{
// Images assume a false ammo state on load. We need to
// set the state according to the current inventory.
if (%obj.getInvItem(%this.ammo.getName(),"amount") !$= "") {

%obj.setImageAmmo(%slot,true);

}

}
#8
07/07/2002 (4:50 pm)
Thanks for the bug report.

Ill update the tutorial with it when I make another update. It was found that the manager doesnt work in MP...I found the reason..at least Im 90% sure I did :) Just have to test it then Ill update the tutorial with both.

-Tim aka Spock
#9
12/03/2002 (5:05 pm)
---
#10
12/03/2002 (5:08 pm)
Hey good work man on your other tuts also!
#11
04/26/2003 (3:47 am)
----------------
#12
05/18/2003 (4:22 am)
replace all the &###1###80; crap with a ' :)
(that is a high comma :P)
#13
01/27/2004 (9:39 am)
I have tried adding the inventory system to my code but I'm getting an "Unknown command" error message for addInvItem and getInvItem. What is happing?
#14
03/03/2004 (4:07 pm)
the latyest build released as of this day im posting, i can't find some of the things in player.cc i have seen it at work before. good work!

EDIT: no, i found the same thins, just a little different wording....
#15
03/15/2004 (5:52 pm)
uh.. "static bool cCheckDismountPoint(SimObject *ptr, S32, const char **argv)" is not in player.cc nor is it in any other part of torque.. am i just stupid?
#16
03/15/2004 (5:55 pm)
nvm :-p
#17
04/10/2004 (8:53 pm)
What would be the easiest way to implement clips into this? I can write the code.. i just need to know were to start.. i'd obviusly need to keep track of the amount of bullets in the clip.. could i just add a Bulletammount to InvItem but then it'd be wasting a whole.. 32 bits of data for every item that isnt a clip.. right?
#18
04/11/2004 (2:00 pm)
I'v got everything coded and scripted.. but now i just need a way to get the amount of bullet the clip can hold.. i know i can add a variable to AddInvItem() but i'm not sure if this would be the best way..

after then i need a way for weapons to store the amount of bullets in their "current" clip.. and then when the weapon is picked up... the ammo as well as the number of bullets it has will be added.. to your inventory..

hem.. if anyone has any idea on how to optimize this.. or a better way or anything.. i'd be more then happy to talk about it..


Btw, i'm just kinda chit chatting right now.. lol
#19
04/11/2004 (11:59 pm)
Ok, I'v gotten all of the code writen.. now i just need to adjust some of the weapon states to work with the new code..

I want to reload when the clip has no more bullets in it.. and i'v looked in shapeImage.cc and i cant figure out whats setting stateTransitionOnNoAmmo to true..

i need it to be set to true only when the bullets in the clip are out..
#20
04/18/2004 (11:12 am)
Heres something i could figure out.. how this works.. whats this code all about?

struct InvItem {

char Name[256];
S32 Amount;
S32 BulletAmount;
char Type[256];
char Description[256];
S32 Max;

};

struct InvLink {

InvItem ItemData;

InvLink* nextLink;
InvLink* lastLink;

};

InvLink* FirstItemInList;
InvLink* CurrentItemInList;
Page «Previous 1 2 3 4 Last »