Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning CSharp Game Programming (2005) [eng]

.pdf
Скачиваний:
151
Добавлен:
16.08.2013
Размер:
4.97 Mб
Скачать

100Chapter 5 One More C# Chapter

Creating Your Own Exceptions

You can create your own exception classes. It’s really easy to do, and you should always inherit the classes from the base System.Exception object (or another built-in exception type, if your needs require it).

class FatalGameException : System.Exception

{

// put some data about the game error in here

}

That’s all there is to it!

Throwing your own exceptions is pretty darned easy, too:

throw new FatalGameException();

In a more complex program, you would make the constructor of the exception take some data about the game, but as this is just a simple example, I omitted that.

Delegates

Delegates are the coolest part of C#, in my opinion. You may disagree with me on this one, but I love delegates. A delegate is an object that points to a function. That’s all there is to it.

In C++, you had function pointers. The idea was that you had a variable that pointed to a function, and you could change the variable to point to other functions at run-time! Function pointers were useful, but ugly as all get-out and difficult to use. I’m not going to waste your time by showing you an example of them, but trust me—they were unbelievably ugly. C#, thankfully, has solved the ugliness-and-difficultness problem completely by creating delegates.

Creating a Delegate

In order to start using delegates, you first need to create a delegate-type definition, in order to tell the compiler what kind of function it’s going to point to. Here’s an example:

public delegate void MyDelegate();

This tells the compiler that you’ve created a delegate-type named MyDelegate, which points to functions that return nothing and have no parameters. Here’s a sample Spaceship class that defines two functions, one static and the other non-static:

class Spaceship

{

public static void InitializeShips()

{

Delegates 101

// initialize all spaceships in the game here

}

public void LaserHit()

{

// get hit by a laser

}

}

Later on, somewhere else, you can create a delegate pointing to those functions. Here’s the code to make a delegate pointing to the static function:

MyDelegate d;

// declare delegate

d = new MyDelegate( Spaceship.InitializeShips ); // create delegate

d();

// calls Spaceship.InitializeShips();

All you do is pass the name of the function (in this case Spaceship.InitializeShips) into the constructor of the delegate.

To create a delegate pointing to a non-static function, do this:

MyDelegate d;

// declare delegate

Spaceship s = new Spaceship();

// create spaceship

d = new MyDelegate( s.LaserHit );

// create delegate

d();

// calls s.LaserHit();

To create a delegate pointing to a non-static function requires a little bit more work; you need to actually have an instance of the Spaceship class. If you try saying new MyDelegate ( Spaceship.LaserHit ) your compiler is going to yell, “I don’t know which spaceship to execute that on, stupid!”

All you need to do is call the delegate as if it were a function, and it will call whatever function you put into it.

Of course, you can create delegates that use different return values and parameters, as well:

class MyClass

{

public static int MyFunction( int a, int b, float c )

{

// some code

}

};

public delegate int MyDelegate( int a, int b, float c );

102Chapter 5 One More C# Chapter

And then later on you could use the delegate like this:

MyDelegate d = new MyDelegate( MyClass.MyFunction ); int x = d( 1, 2, 3.141596 );

Or you could swap out MyClass.MyFunction with any function that returns an int and takes two ints and a float as parameters.

Chaining Delegates

C# took the idea of function pointers and went one step further, introducing delegate multicasting. That’s a nice complex term, isn’t it? I like to use the word chaining, instead, because it’s more descriptive.

Delegate multicasting, or chaining, means that you’re allowed to chain delegates together. Look at this code, for example (which uses the Spaceship class from the example I showed you previously):

MyDelegate d;

// declare delegate

Spaceship s = new Spaceship();

// declare spaceship

d

= new MyDelegate( Spaceship.InitializeShips ); // create delegate

d

+= new MyDelegate( s.LaserHit );

// chain a laserhit call

d();

// call InitializeShips and s.LaserHit!

 

If you execute a chained delegate, the code executes every function inside the delegate in the order that you added it. So the last line of the previous code example will call

Spaceship.InitializeShips and then call s.LaserHit.

You can also remove delegates as well:

d -= new MyDelegate( Spaceship.InitializeShips );

d(); // only calls s.LaserHit now.

I really love this whole idea of chaining delegates. It adds so many possibilities to your programs.

n o t e

If you chain delegates that return values, you may run into some problems. The way C# handles that situation is by returning the value of only the last function inside the delegate. All other return values are discarded. So don’t return things that are absolutely vital to your program if you know the function is going inside a chained delegate.

Collections

Data collections are a huge topic in computer programming, and it deserves a whole book unto itself. I’m going to have to give you a limited rundown of C#’s collections here.

 

Collections

103

You’ve already seen one collection type in Chapter 4: the array. As I’ve already gone over

 

arrays, in the subsections below I’ll just show you the more advanced containers in C#.

 

The ArrayList

 

 

Arrays are great for storing information, but they have a large drawback: you can’t resize

 

them. If you want to resize an array, then you must create a new one, copy it over, and dis-

 

card the old array. That’s a lot of wasted effort.

 

Luckily, the .NET framework provides you with the System.Collections.ArrayList class,

 

which is automatically resizable. It’s fairly simple to use:

 

System.Collections.ArrayList list = new System.Collections.ArrayList();

 

// adding elements

 

 

list.Add( 20 );

// 20

 

list.Add( 30 );

// 20, 30

 

list.Add( 40 );

// 20, 30, 40

 

list.Insert( 1, 60 );

// 20, 60, 30, 40

 

list.Insert( 0, 90 );

// 90, 20, 60, 30, 40

 

// accessing elements

 

 

int x;

 

 

x = (int)list[0];

// 90

 

x = (int)list[2];

// 60

 

// finding elements

 

 

bool b;

 

 

b = list.Contains( 20 );

// true

 

b = list.Contains( 10 );

// false

 

x = list.IndexOf( 90 );

// 0

 

x = list.IndexOf( 30 );

// 3

 

// removing elements

 

 

list.Remove( 30 );

// 90, 20, 60, 40

 

list.RemoveAt( 0 );

// 20, 60, 40

 

list.Clear();

// empty!

 

That example should give you a pretty good idea of how array lists work. It’s not really that difficult.

The only things you should be concerned about are removing and adding items into the middle of the list. Keep in mind that the .NET framework keeps the list stored in an array, so when you insert something in the middle, everything after that spot has to be moved

104Chapter 5 One More C# Chapter

up, and when you remove something in the middle, everything after that spot has to be moved down. It’s not a huge deal, but you should be aware of it.

Another fact to remember is that array lists can store objects of any type. That means that you can perform operations like this, storing many different kinds of objects in the same list:

list.Add( 10 );

list.Add( “I am the very model of a modern major-general!” );

list.Add( new Spaceship() );

This also means that you need to cast objects when you take them out of the list again:

int x = (int)list[0];

string s = (string)list[1];

Spaceship sp = (Spaceship)list[2];

This can be a little tedious if you don’t remember what you stored in your list.

The typeof Operator

If you are not sure of an object’s type, you can perform a test on it like this:

if( list[0].GetType() == typeof(int) )

// element is an int

if( list[1].GetType() == typeof(string) )

// element is a string

if( list[2].GetType() == typeof(Spaceship) )

// element is a spaceship

And that’s how you can be sure.

The is Operator

Another operator you can use on types is the is operator, which is amazingly simple to use:

if( list[0] is int )

//element is an int if( list[1] is string )

//element is a string if( list[2] is Spaceship )

//element is a spaceship

This method is a lot cleaner than the GetType method I showed you previously.

The as Operator

Another way to test the objects in your collection, if you know they are references and not value-types, is to use the as operator. Here’s an example:

Collections 105

Spaceship s = list[2] as Spaceship;

If the element is a Spaceship, then s will hold a reference to the spaceship object inside the list. If it isn’t a Spaceship, then s will hold null instead.

n o t e

You cannot use this operator with value-types, because it works by using references. When it fails, it returns a null reference, and value-types cannot be null.

Hash Tables

Hash tables are an extremely efficient collection type. They store key-value pairs, meaning they act sort of like an array, except you can use anything as the “index,” not only numbers. Here’s a simple example that uses strings as the key:

System.Collections.HashTable table = new System.Collections.HashTable();

//store some data in the table: table[“pi”] = 3.14159; table[“e”] = 2.71828; table[“fourty-two”] = 42.0;

//now retrieve it:

double d;

 

 

d = table[“e”];

// 2.71828

d = table[“fourty-two”];

// 42.0

d = table[“pi”];

// 3.14159

// remove an entry:

 

 

bool b;

 

 

b = table.Contains( “pi” );

// true

table.Remove( “pi” );

 

 

b = table.Contains( “pi” );

// false

I don’t really have the room to go over the internal details of a hash table, unfortunately. But let me give you a brief rundown of how they work. Whenever you insert a key/value pair into a table, the key values are hashed into a numerical value. C# does this by way of calling the GetHashCode function of the key object, which returns an int. All Objects have this function built in, so you can use anything as a key value. C# then treats this hash value as an index into the table, and quickly accesses the item for you.

106 Chapter 5 One More C# Chapter

n o t e

I recommend that you create your own hash functions for your classes, as the built-in method isn’t that good for custom data. Since it is unlikely that you’ll need to use your own key types for a simple game, I’m going to leave this topic up to you to explore more on your own. I wrote an extensive chapter about hash tables in the book Data Structures for Game Programmers; though the book deals primarily with C++, the concepts remain the same no matter what language you use.

Stacks and Queues

The .NET framework also provides stacks and queues, which are simple linear containers that allow you to access data in a specific manner. Mainly, a queue is a first-in-first-out (FIFO) container (objects put in first will be removed first), and a stack is a last-in-first- out (LIFO) container (objects put in last will be removed first).

Here’s an example of a queue:

System.Collections.Queue q = new System.Collections.Queue();

q.Enqueue( 10 );

// 10

 

q.Enqueue( 20 );

// 10, 20

q.Enqueue( 30

);

// 10, 20, 30

q.Enqueue( 40

);

// 10, 20, 30, 40

q.Enqueue( 50

);

// 10, 20, 30, 40, 50

// just look at the top

 

 

int x = (int)q.Peek();

// 10

 

// or look and remove the top

 

x = (int)q.Dequeue();

// 10, q = 20, 30, 40, 50

x = (int)q.Dequeue();

// 20, q = 30, 40, 50

x = (int)q.Dequeue();

// 30, q = 40, 50

x = (int)q.Dequeue();

// 40,

q = 50

x = (int)q.Dequeue();

// 50,

q = {empty}

It’s a pretty simple concept, and it’s pretty handy.

Stacks are almost identical to queues, except in the way items are removed. Rather than take things off the front, stacks take things off the back:

System.Collections.Stack s = new System.Collections.Stack ();

s.Push( 10 );

// 10

s.Push( 20

);

// 10, 20

s.Push( 30

);

// 10, 20, 30

s.Push(

40

);

//

10, 20, 30, 40

s.Push(

50

);

//

10, 20, 30, 40, 50

File Access

107

// just look at the top

 

 

int x = (int)s.Peek();

// 50

// or look and remove the top

 

x = (int)s.Pop();

// 50,

s = 10, 20, 30, 40

x = (int)s.Pop();

// 40,

s = 10, 20, 30

x = (int)s.Pop();

// 30,

s = 10, 20

x = (int)s.Pop();

// 20,

s = 10

x = (int)s.Pop();

// 10,

s = {empty}

Other Collections

There are a bunch of other collections in C#, but the ones I’ve presented here are the collections you’ll be using the most. I’ll let you explore the other collection types on your own, as Microsoft has documented them really well in the MSDN (which can be found online at http://msdn.microsoft.com/). Just do a search for “System.Collections,” and you should find more information than you’ll need.

File Access

Games are complex creatures. The days when you could load up an arcade game and beat it in a few hours are gone; now games exist that literally take weeks of solid playing to complete.

Obviously, no one has enough stamina to play a game that long—that would be insane. So you need a way to store the game data so that a game player can come back and pick up where they left off.

Streams

In virtually any computer system, files are treated as abstract objects called streams. That’s because you usually read or write a long “stream” of data at one time rather than skipping around all over the place.

Streams aren’t just for files, though; they’re used for all sorts of things, such as text input and output and network communication. In this section, however, you’re only going to be concerned with file streams.

Reading and Writing

The base class for all streams in .NET is System.IO.Stream, which is an abstract class. Streams have all sorts of functions, but the functions you’re going to be concerned with the most are Write, WriteByte, Read, and ReadByte.

108 Chapter 5 One More C# Chapter

The functions do exactly what they promise to do, and allow you to read or write arrays of bytes or just single bytes to a stream. For example (assume the stream s is already valid):

byte b;

 

b

=

s.ReadByte();

// read the first two bytes from

b

=

s.ReadByte();

// the stream

s.WriteByte( 10 );

// writes the value 10

s.WriteByte( 255 );

// writes the value 255

byte[] array = new byte[4];

// read into array starting at index 0, and read at most 4 bytes s.Read( array, 0, 4 );

s.Write( array, 0, 4 ); // write everything back out again

This example is kind of silly because you won’t often be combining read and write operations in the same general area of code. Most of the time, you’re either performing one operation or the other, not both. This was just to show you how the functions worked.

Flushing and Closing

The two other major functions that streams support are Flush and Close.

Contrary to what you may think, when you write to a stream, what you write isn’t actually written out immediately. The reason for this is that device access is slow. In terms of processing time required, it actually takes a long time to write one byte out to a file. There’s a lot of overhead in such operations, so the amount of time it takes to write out one byte is only slightly smaller than the amount of time it takes to write out a bunch of bytes.

For this reason, streams use a cache. Whenever you write something to a stream, the computer holds on to it for a bit, and when the cache is full enough, the contents of the cache are finally written out to the actual device it’s attached to. Unfortunately, this can be a pain in the butt because you have no idea when this is actually going to happen. Sometimes you need to make sure that something gets written out immediately. That’s what the Flush method is for. Take a look:

s.WriteByte( 20 );

// cache a 20

s.Flush();

// make sure cache is manually written out immediately

n o t e

Tests on my computer show that my file streams cache 1024 bytes before automatically writing them out to disk. Your results may vary.

File Access

109

And, of course, the Close function simply closes the stream so that no more reading or writing can be performed. Closing a stream also flushes it.

Readers and Writers

Reading and writing raw bytes to a stream gets tedious really quickly. You’re going to want to read and write more important things, like ints and floats, or even text strings. Because of this, the .NET framework includes specialized classes, called readers and writers, that perform the reading and writing of datatypes automatically for you. These classes encapsulate a stream and allow you to read and write specialized data to a stream.

Text and Streams

The easiest pair of classes to use are the System.IO.StreamReader and System.IO.StreamWriter classes. They allow you to read and write strings to and from a file. Text files are usually called ASCII files, as they use the ASCII formatting standard—though that’s not always the case. The .NET framework allows you to change the formatting, but you’re not really going to be concerned with that unless you’re doing international game development.

Reading

When reading from a text stream, there are only a few functions you should be concerned with: Read, ReadLine, and ReadToEnd.

Read reads a specified number of characters from a stream, stores them into a character array, and returns the total number of characters read. Here’s an example (assume that StreamReader s is already set up with an underlying device; I’ll show you how to do that in a little bit):

char[] buffer = new char[32];

int x;

x = s.Read( buffer, 0, 32 );

// read up to 32 characters and store them

// in buffer starting at index 0.

The x variable will now hold the number of characters actually read (if the stream doesn’t have 32 characters to read, it will return as many as it can), and the buffer array will have everything that has been read.

An easier function to use is the ReadLine function:

string str;

 

str = s.ReadLine();

// reads the next line of text

That’s all there is to it. The function returns a string, making things nice and easy for you.