Home Lawrence's Labyrinth Deconstructing B/X D&D Adventure Generator! B/X D&D Character Generator Other Contact
 

First PagePrevious PageBack to overviewNext PageLast Page

Classes, Objects, and the Object Graph

Let's talk a little about Object Oriented Programming, and then look into writing some more PlayerCharacter constructors!

Objects

In Object Oriented Programming, we build our software as a set of Objects. As we touched on previously, objects have their data encapsulated within them, and provide methods through which we can manipulate the objects and their data.

The Object Oriented paradigm is particularly useful for visualizing and designing games like this!

Each “thing” that exists in the game is an Object. The player character is an Object. The goblin that the character is fighting with is another Object. The character's sword and shield, and the goblin's spear are all different objects.

The Object Graph

The relationships between all of these objects, the Object Graph, defines the state of the game world. The player character and the goblin are within a chamber in the dungeon. The player character is holding his sword and shield, and the goblin holding his spear.

If you imagine lines connecting all of these objects together, then it is easier to visualize the object graph!

There is nothing stopping the object graph from being circular! That is, it is entirely possible to program something where a Box contains a Sack, while at the same time the Box is also inside the Sack!

M. C. Escher would have been all over that, but it makes very little sense in a piece of software that models a game world!

A Tree-Like Structure for Modeling the Game World

So, I like to distill my object graph (at least insofar as it pertains to modeling the game world) into a Tree, rather than an arbitrary cyclic graph!

For example, we could say that a Dungeon contains a number of Rooms and Passageways. These rooms and passageways could contain Treasures, Monsters, Player Characters, Chests, Furniture, etc. The player characters and monsters have an inventories, and “contain” whatever they are carrying. They may even be carrying a sack or a backpack that contains yet more objects!

See how it all fans out into a tree-like structure, with nothing circularly referencing anything else? If we write methods to move objects around in the game world, and make sure that those methods enforce that the game world's object graph maintains the tree-like structure, then things are a heck of a lot easier to visualize and we write less buggy code!

Classes

We've been writing all of these “classes”, but haven't really touched on what a class really is!

There's nothing to it, though! A Class is simply a blueprint for an object. We define a class, and then we can instantiate (that is, “create”) any number of objects of the type that the class defines.

Creating Objects

Objects are instantiated using the “new” keyword.

		// Generate a character. 
		Critter character = new PlayerCharacter(); 

This instantiates a fresh, new PlayerCharacter object, from the definition provided by the PlayerCharacter class, and a reference to the object is stored in the variable called “character”.

When an object is instantiated, its Constructor is called, to set up the object. The constructor is the method that has the same name as the class. Let's modify the constructor of PlayerCharacter.java:

	/** 
	 * Rolls a new character. Stats are straight 3d6 down the line. Name, 
	 * gender, and alignment are set to the specified values. If any of those 
	 * values are null, then that value will be rolled randomly. 
	 */ 
	public PlayerCharacter( String name, Gender gender, Alignment alignment ) 
	{ 
		// Roll stats. Straight 3d6. 
		for( int i = 0; i < this.stats.length; i++ ) 
			this.stats[i] = Dice.d( 3, 6 ); 
		if( gender == null ) 
		{ 
			// Roll randomly for gender. 
			if( Dice.d( 2 ) == 1 ) 
				this.gender = Gender.MALE; 
			else 
				this.gender = Gender.FEMALE; 
		} 
		else 
		{ 
			// Explicitly set the gender. 
			this.gender = gender; 
		} 
		if( alignment == null ) 
		{ 
			// Roll randomly for alignment. 
			int roll = Dice.d( 3 ); 
			if( roll == 1 ) 
				this.alignment = Alignment.LAWFUL; 
			else if( roll == 2 ) 
				this.alignment = Alignment.NEUTRAL; 
			else 
				this.alignment = Alignment.CHAOTIC; 
		} 
		else 
		{ 
			// Explicitly set the alignment. 
			this.alignment = alignment; 
		} 
		if( name == null ) 
		{ 
			// Instantiate some tables to roll names on, using the generated 
			// gender 
			// and alignment. 
			Table names = new NamesTable_Barbarians( this.gender ); 
			Table titles = new TitlesTable_LegendaryCritters( this.gender, this.alignment ); 
			// Roll randomly for name and title. 
			this.name = names.random() + ", " + titles.random(); 
		} 
		else 
		{ 
			// Explicitly set the name. 
			this.name = name; 
		} 
	}

Then, let's add a second constructor!

	/** 
	 * Rolls a new character, completely randomly. Stats are straight 3d6 down 
	 * the line. 
	 */ 
	public PlayerCharacter() 
	{ 
		this( null, null, null ); 
	}

That's two constructors, taking different arguments! So we can create characters in different ways:

// This creates a completely random character, using the constructor 
// that takes no arguments! 
Critter pc1 = new PlayerCharacter();
// This one calls the constructor that creates a character using the 
// arguments we specify! 
Critter pc2 = new PlayerCharacter( "Warrior McHenchman", Gender.MALE, Alignment.NEUTRAL );
// With the code that we added to out constructor, passing null values 
// makes it roll those items randomly!  So this line will create a 
// Chaotic character with a random name and gender! 
Critter pc2 = new PlayerCharacter( null, null, Alignment.CHAOTIC );

That last line might create something like this:

Brynhildr, the Mistress of Fear 
Chaotic Level 1 Female Barbarian 

Strength: 13 (+1) 
Intelligence: 8 (-1) 
Wisdom: 11 
Dexterity: 16 (+2) 
Constitution: 15 (+1) 
Charisma: 14 (+1)

She looks pretty scary! I sure wouldn't want to run into her in a dark creepy forest! … … Or maybe I would! :3

… … Anyway, if you look closer at the code, you'll see that the only one of the constructors actually does anything. The other ones shortcut to it using this().

	public PlayerCharacter() 
	{ 
		this( null, null, null ); // <-- Passes off to another constructor, 
                                         //     the one that takes arguments of the 
                                         //     type given here (Gender,Alignment). 
	} 

We've just touched on Method Signatures. The TL;DR of method signatures in Java is: You can define several methods with the same name, as long as they take different arguments. The appropriate method will be called based on the arguments you specify when you call the method (but the arguments must be in the same order as they are in the method definition, of course).

Object References

References are something that often trip up newcomers. If that describes you, then it's definitely worth going over this section until you grok it, or you may find yourself in a world o' pain later on and not be able to figure out why!

After creating the object with the “new” keyword above and assigning it to the “character” variable, the “character” variable does not contain the object! It contains a reference to the object, which is not the same thing.

Let's look at a practical example to better illustrate this!

// Create a player character and assign a reference to it in the "character" variable. 
Critter character = new PlayerCharacter(); 
 
// Assign the reference to a second variable. 
Critter c2 = character; 
 
// This does NOT make a copy of the character, nor does it create a new one! 
// "character" and "c2" now refer to the SAME character. 
 
character.adjustCurrentHP( -2 ); 
c2.adjustCurrentHP( -3 ); 
// The character has now taken a total of 5 points of damage, since 
// "character" and "c2" refer to the same object! 

Once you are done with an object (that is, when it is no longer referenced by any variables, anywhere), the Java runtime will clean it up magically in the background and make the memory it used available for creating more objects. You don't have to explicitly free() or delete() memory like you do in some other languages.

Test It!

Let's change our Test.java again!

	@Override 
	public void run( SimpleIO io ) 
	{ 
		Alignment alignment = null; 
		for( ;; ) 
		{ 
			// Clear the screen. 
			io.clear(); 
			// Generate a character of the specified alignment. 
			Critter character = new PlayerCharacter( null, null, alignment ); 
			printCritter( io, character ); 
			String input = null; 
			boolean selectionIsValid = true; 
			do 
			{ 
				selectionIsValid = true; 
				io.println(); 
				io.println( "Press Enter to generate a random character." ); 
				io.println( "Or type 'L', 'N', or 'C', followed by Enter, to create a character of Lawful, Neutral, or Chaotic alignment." ); 
				input = io.getInputLine().toUpperCase(); 
				if( "".equals( input ) ) 
					alignment = null; 
				else if( input.startsWith( "L" ) ) 
					alignment = Alignment.LAWFUL; 
				else if( input.startsWith( "N" ) ) 
					alignment = Alignment.NEUTRAL; 
				else if( input.startsWith( "C" ) ) 
					alignment = Alignment.CHAOTIC; 
				else 
					selectionIsValid = false; 
			} while( !selectionIsValid ); 
		} 
	} 
 
	private static void printCritter( SimpleIO io, Critter critter ) 
	{ 
		// Print the character's name. 
		io.println( critter.getName() ); 
		// Print the character's level, alignment, gender, and class. 
		// (Everyone is a barbarian for now!) 
		io.print( caps(critter.getAlignment().name() ) ); 
		io.print( " Level 1 " ); 
		io.print( caps(critter.getGender().name() ) ); 
		io.println( " Barbarian" ); 
		io.println(); 
		// Loop through all of the character's stats and print them. 
		for( Stat stat : Stat.values() ) 
		{ 
			// The name of the stat, with capitalization fixed, followed by the stat's value. 
			io.print( caps( stat.toString() ) + ": " + critter.getStat( stat ) ); 
			// If the stat modifier is nonzero, then we print it, too. 
			int mod = critter.getStatModifier( stat ); 
			if( mod != 0 ) 
			{ 
				io.print( " (" ); 
				// Positive numbers don't automatically print with a "+" in front of them, so we add it. 
				if( mod > 0 ) 
					io.print( "+" ); 
				io.print( mod ); 
				io.print( ")" ); 
			} 
			io.println(); 
		} 
	} 
 
	// This is a silly function that capitalizes the first letter of a string, 
	// and lowercases all the rest. We use it when we print out enumeration 
	// constants, because they are generally ALL-CAPS, and that is ugly! 
	private static String caps( String s ) 
	{ 
		return s.substring( 0, 1 ).toUpperCase() + s.substring( 1 ).toLowerCase(); 
	}

There are some interesting things going on here that are worth looking at!

Firstly, we moved the critter printing code out into its own method. This makes our code cleaner and more readable!

Secondly, we're reading input from the keyboard and doing something with it. input = io.getInputLine().toUpperCase();

Thirdly, once we've gotten the input line from the keyboard, we start by comparing it with an empty string, to see if the user just pressed Enter without typing anything. if( “”.equals( input ) )

Why didn't we use if( input == “” )?

It's a reference thing! The “==” operator, when applied to two objects (the empty “” string is a String object, yes) compares object references, not the values contained in the objects (ie, the text of the string in this case). Since Java does not allow operator overloading/overriding (this is either a Feature or a Bad Thing™, depending on who you ask), all objects provide an equals() method, which we can override in our own classes, to compare the values of objects instead of their references.

Here's an example.

String s1 = "Foo!"; 
String s2 = "Foo!"; 
 
// This block will print "== is false!", because s1 and s2 are different objects. 
// (Well, technically since string constants are final, the compiler might roll them 
// into the same object, but there's no guarantee!) 
if( s1 == s2 ) 
    io.print( "== is true!" ); 
else 
    io.print( "== is false!" ); 
 
// This block will print "equals() is true!", because equals() compares the actual 
// object values, instead of their references. 
if( s1.equals(s2) ) 
    io.print( "equals() is true!" ); 
else 
    io.print( "equals() is false!" );

Now then, why did we use “”.equals( input ) above instead of input.equals( “” )? The latter is more readable, isn't it?

Because “”.equals( input ) will return false if input=null. input.equals( “” ) will throw a NullPointerException and crash your program when input=null, because equals() is a method defined in the String object, and when there's no object (null) to call a method out of, it causes a runtime error!

So, always compare your strings using the “some string constant”.equals( someStringVariable ) method, even though it looks a little weird, and your code will be much more robust!

What's Next?

In the next chapter, we'll dive into something a little more intermediate: expanded enumerations and gender-specific pronouns!

Source Code

The source code for this chapter can be found in the Subversion Repository.

If you are interested in changing/modifying the code, you can download it into your Eclipse workspace using Subclipse as detailed here: TUTORIAL: Using Subclipse.

First PagePrevious PageBack to overviewNext PageLast Page

rustybox/book/007_classes_objects_and_the_object_graph.txt · Last modified: 2012/09/09 00:54 by leaf

Copyright © 2009, 2012-2014 by L. Adamson, unless otherwise stated.
If you see something here that you like, and find it useful or learn something, please consider making a quick, easy, and secure donation via PayPal.
Your support is what keeps this whole thing going!