Post tutorial RSS Adding enemies

This document discusses the procedure of integrating a new enemy class in the Shogo source code. It doesn't concern itself with weapons, or effects and it doesn't concern itself with actually coding the behavior of a new enemy. It only concerns itself with all the changes you have to make into the engine to make your new enemy available in the game. Programming the actions of an enemy would probably the subject of a future tutorial, and maybe a series of tutorials.

Posted by on - Intermediate Mapping/Technical

Originally posted here: Planetshogo.com

Created by: Shawn "ARTMAN" LeBlanc

Mirrored here for archival purposes

Shogo Enemy Integration Document
Version 1.0
March 14th, 1999

by Shawn "ARTMAN" LeBlanc
wrlddomprod@hotmail.com

Contents
1. Introduction
2. Description
3. Disclaimer
4. Version Information
5. Warning
6. What You Need
7. Assumptions
8. The Changes (aka the good part)
9. When You're Done
10. Conclusion

1. Introduction

Greetings! This document came out of the desire to start writing down the ins and outs of Shogo mod coding. It also came out of the fact that I still can't remember all the things I need to do to implement stuff, so I write them down. And since I was writing it, I figured I'd share it.

2. Description

This document discusses the procedure of integrating a new enemy class in the Shogo source code. It doesn't concern itself with weapons, or effects and it doesn't concern itself with actually coding the behavior of a new enemy. It only concerns itself with all the changes you have to make into the engine to make your new enemy available in the game. Programming the actions of an enemy would probably the subject of a future tutorial, and maybe a series of tutorials.

3. Disclaimer (this claim her)

I'm am not responsible for any bad things that happen due to the use of this document. I also take no responsibility for the actions of this document, even if it jumps out of your computer and goes on a killing spree or messes up your hair. You've been warned.

4. Version Information

1.0 First version of this document.

5. Warning

I'm very new to the Shogo mod community, so I am probably missing tons of stuff and I might be misinformed about certain things. Feel free to write to correct any mistakes I've done.

This is the first version of this document, and as such, is bound to be modified and improved. Please feel free to come up with suggestions and ideas. You can contact me at wrlddomprod@hotmail.com

6. What you Need

  • You need to get the latest source and tools. You can find them at www.planetshogo.com or at www.lith.com..
  • You also need Microsoft's Visual C++, because you just can't code with notepad.
  • Oh, and a reasonable computer. Those using 386's will be pointed out and laughed at.

7. Assumptions

This isn't your standard beginner document, so I'm making some assumptions, like:

  • You know C++ and Microsoft Visual C++'s development environment
  • You know how decompress, use and compile the source.
  • You've decompressed the Shogo rez files.
  • You know how to use DEdit so you can test your work.
  • You've either written a new object class with its properties set to your liking, or you've just copied an old class and renamed it. The last one might be easier when starting to test this out.
  • You're adding a completely new object, so that means it has its own new model, skins and gib parts. All the appropriate files are placed in the right directories. Again, you can just copy and rename an existing character's model, skins and gibs if you don't have a 3D artist nearby. :)

8. The Changes (aka the good part)

Before we being, make sure that the files in the Shared directory are actually "shared" between the Object.lto source project and the CShell.dll project (they use the exact same files). This saves a lot of time because changes from one project will automatically go to the other. The VC++ project files included don't do this correctly, so you'll have to do it yourself. Fire up both projects and let's begin.

The changes to be made have to be done in several functions and header files. The files you'll have to modify are:

ModelIds.h
ModelNodes.h
ModelFuncs.cpp
ModelIds.h

Basically, this is the first thing you need to modify. You just add a new ID in the ModelIdType enumeration. For this and the other examples, I'll assume we're working on an enemy called "Cheryl". Cheryl is the nickname I use, for no apparent reason, for when I play Quake. As it is readily apparent, the name strikes fear into the hearts of my enemies. :) Now let's move on.

Add the new id right before MI_LAST. Here's how the code would look like with the new ID:

enum ModelIdType
{
  MI_FIRST = 0,   MI_PLAYER_
  ... // one, two, skip a few
  MI_AI_SAMANTHA_ID,

  MI_AI_CHERYL_ID  // NEW MONSTER ID. 

  MI_LAST,
  MI_UNDEFINED
}

(remember that the '...' means I'm just skipping typing the other stuff. I'm just lazy)

I suggest following the naming convention (MI_AI_blah_ID) because it keeps the code style consistent. Now Cheryl has its own id that identifies it inside the game.

Don't forget to check the BEGIN_CLASS/END_CLASS properties of your new enemy so that it uses the new ID.

ModelNodes.h

The second file to be modified is the ModelNodes.h file. This file contains every character's list of nodes. A node is a piece of the monster's model, like its upper leg, or head. Open up Model Edit and look at one of the character models from Shogo. As you can see, the first scroll box has a bunch of names like "head" and "torso". If you click on one, you'll see the appropriate piece highlighted in the model view window. That is one node of the model. What you're going to add is a list of the names of all the nodes of your enemy model.

The model list is important because the game needs to know what part of the model is being hit so it can play the appropriate recoil animation and apply any specific area damage. I tried implementing a new enemy once, and I forgot to add the new node list. The result was that he couldn't be shot dead, so it's kind of important. :)

All you have to do is to create a static array of strings and each string is the name of one node. Just add the new one at the end of the last one, right after "s_TenmaModelNodes".

For Cheryl, her node list it would look something like this:

static char *s_CherylModelNodes[] =
{
     "torso",
     "left_armu_1",
     "left_arml_1",
     "left_arml_hand",
     "head_1",
     "head_2",
     "pelvis",
     "left_legu_3",
     "left_legl_1",
     "left_legl_shoe",
     "right_legu_3",
     "right_legl_1",
     "right_legl_shoe",
     "right_legu_1",
     "right_legu_2",
     "left_legu_1",
     "left_legu_2",
     "pelvis_3",
     "pelvis_2",
     "pelvis_4",
     "right_armu_1",
     "right_arml_1",
     "right_arml_hand"
};

The nodes names aren't the same for every model, so milage may vary.

One thing to notice is that the list doesn't contain the null nodes of the model, like the GUN_HAND node. I'm not sure why, but it's probably because the other nodes are used for other things and not collision detection, like where to place the gun model on the character.

Again, we're keeping the naming convention for consistency.

The next things you'll need to change can be found later in the ModelNodes.h file, the GetNumModelNodes() and GetModelNodeName() inline functions.

GetNumModelNodes() basically returns the number of nodes in the enemy's node list. They're just doing it in a funky way. They do use a lot of funky (read: interesting) methods in the Shogo code. It's good stuff to learn. What you need to add is a case statement for the new enemy, right before the vehicles section to keep everything organized. For Cheryl, it would look like this:

//Shawn's added stuff
     case MI_AI_CHERYL_ID:
          nRet = (sizeof(s_CherylModelNodes) / sizeof(s_CherylModelNodes[0]));       break;

Basically, it returns the size of the array divided by the size of one node name, which would give you the number of nodes in the list. Funky, eh? It's a good method because the engine never knows how many nodes each character has.

GetModelNodeName() returns the name of the node given by the first parameter, dwIndex. The second parameter, nId, specifies which object it wants to use. Again, you just add a new case in the switch statement. Place the new case before the Vehicles section so it's consistent. For Cheryl, the added case statement would look like this:

     case MI_AI_CHERYL_ID:
          pRet = s_CherylModelNodes[dwIndex];
     break;

ModelFuncs.cpp

This is the third and last file that needs to modified to get Cheryl into the engine. ModelFuncs.cpp contains functions that return information about one of Shogo's objects, like its model name or skin. The functions that need to be changed are:

GetModel()
GetSkin()
GetGibModel()
GetModelType()
GetModelName()

GetModel() just returns the filename of the model associated with the id provided. It's just a big switch statement. For Cheryl, I inserted this code right after the case for MI_PLAYER_ENFORCER_ID:

     case MI_AI_CHERYL_ID:
          pRet = "Models\\Enemies\\Onfoot\\Cheryl.abc";
     break;

The pRet string variable is assigned with the filename and then is returned by the function.

GetSkin() is similar to GetModel(), except it just returns the skin associated with the provided it. Again, it's just a big switch statement. For Cheryl, I added this in the AI SKINS section:

     case MI_AI_CHERYL_ID:
          pRet = "Skins\\Enemies\\Cheryl.dtx";
     break;

Note that there's a slight twist to this function. If you want, you can have multiple skins for the same model, depending on "alignment". Cheryl can have different skins depending if she's with the UCA, Shogo, the FALLEN or whatever. The alignment of the character is given with the variable "CharacterClass cc". To add support for different alignments, you just need to insert another switch statement inside the case statement, like this:

     case MI_AI_CHERYL_ID:
          switch (cc)
          {
          case SHOGO:
               pRet = "Skins\\Enemies\\Cheryl_SHOGO.dtx";
          break;
          case CMC:
               pRet = "Skins\\Enemies\\Cheryl_CMC.dtx";
          break;
          case UCA:
          case UCA_BAD:
               pRet = "Skins\\Enemies\\Cheryl_UCA.dtx";
          break;
          case FALLEN:
               pRet = "Skins\\Enemies\\Cheryl_FALLEN.dtx";
          break;
          case CRONIAN:
               pRet = "Skins\\Enemies\\Cheryl_CRONIAN.dtx";
          break;
          case STRAGGLER:
               pRet = "Skins\\Enemies\\Cheryl_STRAGGLER.dtx";
          break;
          case ROGUE:
               pRet = "Skins\\Enemies\\Cheryl_ROGUE.dtx";
          break;
          case BYSTANDER:
          default :
               pRet = "Skins\\Enemies\\Cheryl_BYSTANDER.dtx";
          break;         
          }
     break;

Don't forget to actually have all those skins for the model. If the engine asks for a skin that isn't there, your model will be naked! Actually, it will render the model and shade it with a solid color.

GetGibModel() returns the filename of the gib piece model for a specified object. nID specifies the object that needs to be gibbed and eType specifies the section of the object. eType is of the type GibType. GibType is an enumeration that lists the major parts of a model. You can find it in GibTypes.h. It looks like this:

     enum GibType { 
          GT_FIRST = 0,
          GT_HEAD = 0, 
          GT_LEFT_ARM, 
          GT_RIGHT_ARM, 
          GT_LEFT_LEG, 
          GT_RIGHT_LEG, 
          GT_UPPER_BODY, 
          GT_LOWER_BODY, 
          GT_BODY,
          GT_LAST 
     }

As you can see, it just lists the major parts (head, left/right arm, left/right leg and upper/lower body). GT_FIRST, and GT_LAST are just there for bounds checking.

Now, with the given object id and gib part id, you need to return the filename of the gib's model. To do this, you just add another case in the big switch statement. I stuck this in the HUMAN MODELS section:

     case MI_AI_CHERYL_ID:
     {
          char* GibFiles[] = 
          {
               "Models\\Gibs\\Cheryl\\Cheryl_head.abc",
               "Models\\Gibs\\Cheryl\\Cheryl_larm.abc",
               "Models\\Gibs\\Cheryl\\Cheryl_rarm.abc",
               "Models\\Gibs\\Cheryl\\Cheryl_lleg.abc",
               "Models\\Gibs\\Cheryl\\Cheryl_rleg.abc",
               "Models\\Gibs\\Cheryl\\Cheryl_ubody.abc",
     "Models\\Gibs\\Cheryl\\Cheryl_lbody.abc",
               "Models\\Player\\Cheryl.abc"
          };
          if (GT_FIRST <= eType && eType < GT_LAST)
          {
               pFile = GibFiles[eType];
          }
     }
     break;

In the new case statement for Cheryl, an array of strings is created. Each string is the filename of a gib model. Make sure you keep the order of the gib names right and that you have the gib models already done. The order of the array is as follows:

  1. head gib model name
  2. left arm gib model name
  3. right arm gib model name
  4. left leg gib model name
  5. right leg gib model name
  6. upper body gib model name
  7. lower body gib model name
  8. whole body model name

Failing to follow the order will result in having the wrong gibs flying around the room, and that would be bad.

The if statement just does some range checking before assigning the gib model name to pFile, a string that is returned by the function.

GetModelType() is a simple function that just returns the "type" of a Shogo object. Model types are MT_MECHA, MT_HUMAN, MT_VEHICLE and MT_PROP_GENERIC. Since Cheryl is "human", we'll add an extra case statement to the HUMAN MODELS section, right after "case MI_AI_COTHINEAL_ID:" making the code look like this:

     ...
     case MI_AI_TROOPER_ID:
     case MI_AI_ETROOPER_ID:
     case MI_AI_COTHINEAL_ID:
     case MI_AI_CHERYL_ID:  // New Enemy
          eType = MT_HUMAN;
     break;

The variable eType is then later returned by the function GetModelType().

GetModelName() is the last function to be modified. It returns the model name of an object, without path or extension. Actually, this function is barely used. It's only used in the SetupFileBuffer() function, in AISounds.cpp. As with everything else, you just add a new case into the switch statement. I just stuck this in the HUMAN MODELS section:

     case MI_AI_CHERYL_ID:
          pName = "Cheryl";
     break;

And that's it.

9. When You're Done

Compile both the Objects project and the CShell project with the changes and then make sure to bring them into Shogo and DEdit. Place your new enemy in a level so you can test it and then fire up Shogo with your level. If everything was done correctly, you should have new friend to play with!

10. Conclusion

Now that wasn't very hard, now was it? Next time, maybe, I'll be back with some more Shogo coding goodness!

Post a comment
Sign in or join with:

Only registered members can share their thoughts. So come on! Join the community today (totally free - or sign in with your social account on the right) and join in the conversation.