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

Beginning CSharp Game Programming (2005) [eng]

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

90Chapter 5 One More C# Chapter

class AdvancedCombatShip : CombatShip

{

new public void LaserHit()

{

// some code

}

}

This code creates two classes using the ISpaceship interface, implementing the LaserHit function. This code doesn’t use virtual functions; CombatShip.LaserHit is just a regular function.

Here’s some example code that shows how the ISpaceship interface works:

ISpaceship s = new CombatShip();

s.LaserHit();

//

calls

CombatShip.LaserHit

s = new AdvancedCombatShip();

s.LaserHit();

//

still

calls CombatShip.LaserHit

The computer doesn’t see AdvancedCombatShip.LaserHit when you’re using the ISpaceship interface. In order to do that, you need to make it virtual:

class CombatShip : ISpaceship

{

virtual public void LaserHit()

{

// some code

}

}

class AdvancedCombatShip : CombatShip

{

override public void LaserHit()

{

// some code

}

}

Now the following code will work the way you expect it to when you use it with the new

CombatShip and AdvancedCombatShip definititons:

ISpaceship s = new CombatShip();

s.LaserHit();

//

calls

CombatShip.LaserHit

s = new AdvancedCombatShip();

s.LaserHit();

//

calls

AdvancedCombatShip.LaserHit

Interfaces 91

Sometimes the default behavior of non-virtual functions in interfaces can work to your advantage; other times it may not. Just keep in mind that with interfaces you have more flexibility with your functions, but with abstract classes you’re forced to use virtual functions.

Access

You can hide functions and data inside of abstract classes. Abstract functions don’t have to be public; you can make them protected, if you wish.

Functions inside of interfaces on the other hand, are always public. The idea is that interfaces define functions that you want everyone to see.

Multiple Inheritance

Multiple inheritance is one of those ideas that seems sensible at first, but once you get to using it, it ends up annoying the heck out of you, which is why C# doesn’t support it directly. However, C# does support a limited form of multiple inheritance, so I’m going to show you the theory behind it. Basically, the idea is that you can create a class that inherits from two different classes.

For example, you may want to create a flagship for your fleet of spaceships; the flagship will be a ship that combines both the weaponry of a combat ship and the cargo capacity of a cargo ship. Sounds great in theory, right? (See Figure 5.1.)

Figure 5.1

Using multiple inheritance to combine two classes into one.

Let me tell you right now: C# does not support multiple inheritance. It’s just a huge mess. Let me give you an example showing you why multiple inheritance is a silly idea. Look at Figure 5.2; it shows diamond inheritance. In the figure, multiple inheritance doesn’t seem like a major problem. We can all agree that a flagship is a spaceship, so what’s the big deal?

92 Chapter 5 One More C# Chapter

Figure 5.2 Diamond inheritance, in which two classes share a common base class.

Think about this: All spaceships can be hit by lasers. But combat ships get hit by lasers differently than cargo ships. So you have two different kinds of ships that can be hit, but they both react completely differently.

Now you make a flagship, which can also be hit by lasers. But how does it do it? Does it get hit by lasers like a combat ship or like a cargo ship? Who knows? There’s really no easy way to concretely define exactly what should happen. The problem gets even worse when data is involved…but enough about that—multiple inheritance is just a pain in the neck.

C# has interfaces to solve the problem of multiple inheritance. In C#, a class can only inherit from one base. If you do the following, you’ll get an error:

class FlagShip : CombatShip, CargoShip

But what if you defined an interface for the cargo ship instead?

interface ICargo()

{

void LoadCargo(); void DumpCargo();

};

Now try inheritance:

class FlagShip : CombatShip, ICargo

Interfaces 93

It works. Now a flagship is essentially a combat ship that can load and dump cargo. Unfortunately, you’re going to have to recode the cargo functions on your own; that’s just one of the limitations you’ll have to deal with.

n o t e

A better solution to the problem of combining capabilities would be to make all spaceships capable of handling cargo in the first place. For example, you could design combat ships to be able to carry a very small amount of cargo, such as the pilot’s personal belongings, while a cargo ship can hold megatons of stuff. It’s up to you.

Generally speaking, you really won’t need multiple inheritance much; other solutions exist that work probably better.

Extending and Combining Interfaces

This concept is so simple that you’ve probably already figured it out: You can extend and combine interfaces. Say you’ve got a combat interface that can shoot lasers and missiles, but later on you want to make a special combat interface that can shoot nukes, too.

You could do this:

interface ICombat

{

void ShootLaser(); void ShootMissile();

}

interface INukeCombat : ICombat

{

void Nuke();

}

Now you have an INukeCombat interface that has three functions: one for shooting lasers, one for shooting missiles, and one for nuking a planet from orbit (it’s the only way to be sure).

Of course, you can combine interfaces as well:

interface IFlagShip : ICombat, ICargo

{

// insert code here

}

Now you have a flagship interface that has functions to perform combat and cargo operations. Nothing special here.

94 Chapter 5 One More C# Chapter

Exceptions

I remember the bad old days of computer programming, before exceptions came along. I’m so glad those days are gone. I can remember writing code that looked like this:

error = Start Graphics Engine(); if( error )

display error message

error = Set Resolution(); if( error )

display error message

error = Start Sound Engine(); if( error )

display error message

And so on. Maybe you still write code like that. If you do, you should stop; it’s a pain in the butt. Exceptions are the solution to ugly code like that. Exceptions are special events that occur in exceptional circumstances. The idea is that you should write your code assuming that it’s going to work, and have the error-handling portion somewhere else.

Let me explain. When you’re running a program, many bad things can happen. You can run out of memory, for example, or a hardware device could fail. Or maybe a file that you need seems to have inexplicably disappeared. You’d better be prepared for when things go wrong.

Unfortunately, programmers are lazy. Admit it: you’re lazy and you hate doing tons of work. Error-checking is just plain annoying and you don’t want to deal with it.

The biggest kick in the butt, however, is realizing that 90 percent of everything you need to protect against almost never happens. Running out of disk space happens once in a blue moon. Some people think, “Well, as it almost never happens, I’m not going to protect against it.” But that sort of thinking can come back to haunt you because sooner or later, it will happen. Nothing sucks more than having a program—especially a game—crash after it’s been accumulating data for a long time. You need to protect your games, that’s all there is to it.

Exception Basics

Exceptions are complex beasts, so I’ll give you a simple overview of them first, and then I’ll go on to the more advanced parts.

An Example of Exceptions

Take a look at this example:

Exceptions 95

try

{

StartGraphicsEngine();

SetResolution();

StartSoundEngine();

}

catch

{

// print an error message

}

The first thing you should notice is that this code is much cleaner than the example I showed you previously. You don’t have annoying error-checking code mixed in all over the place. The main code is clean, and you can quickly see what it does. Start the graphic engine, set the resolution, start the sound engine. Badda-bing, badda-boom.

Obviously, if a problem occurs and one of those tasks can’t be accomplished, then the game can’t run. That’s an exceptional circumstance.

What happens when an exceptional circumstance occurs is that each of those functions will throw an exception when something bad happens (I’ll show you how to throw exceptions later). When an exception is thrown, the function immediately exits and execution keeps jumping out of each function until it finds a catch block. So, in the code I just showed you, if something goes wrong and you can’t start the graphics engine inside of StartGraphicsEngine, then the function will throw an exception and execution will jump out of the function immediately, skip over SetResolution and StartSoundEngine without executing them, and start executing the code inside the catch block.

The try Block

The first part of the code example I showed you previously is the try block. A try block is a piece of code that tells the compiler, “I want to execute this code, but I know something bad may happen inside of it, so watch out for an exception.” Every try block must be followed by a catch block, which I’ll get to in the next section.

So what happens if you execute code that might throw an exception when it’s not inside a catch block? Look at this code, for example (as usual, assume it’s inside of a class):

public void Initialize()

{

StartGraphicsEngine();

SetResolution();

StartSoundEngine();

}

96Chapter 5 One More C# Chapter

Now, say someone calls Initialize. That calls StartGraphicsEngine, which tries to find a display device (note that this code is all just make-believe, serving only as an example). Assume that it can’t find a display device. That’s a pretty darn exceptional error, so the function says, “I give up, I can’t start the graphics engine” and throws an exception. The code execution jumps out, back into the Initialize function. Rather than continuing, it sees that there is no try/catch block to handle the exception, and it jumps out again to whomever called Initialize.

Exceptions will keep jumping up until they find a try/catch block that can handle the exception. If they can’t find one, then the entire program will eventually exit. The following is from Code Example 5.1, which can be found on the CD:

public class Class1

{

static void Main( string[] args )

{

int[] array = new int[5]; array[100] = 20;

System.Console.WriteLine( “This should never be executed” );

}

}

The example creates a new array with five indexes (0 through 4), and then tries assigning the value 20 to index 100.

The C# array class is smart; it realizes that you’re doing something that just plain isn’t right—the array isn’t that big! Something really exceptional must have happened, so the array class throws an exception. The last line of code is skipped, and the program just exits out. You didn’t catch the exception.

The catch Block

To catch exceptions and prevent your programs from spiraling out of control, use catch blocks. Here is Example 5.2 from the CD, which is a reworking of Example 5.1:

public class Class1

{

static void Main(string[] args)

{

int[] array = new int[5];

try

{

array[100] = 20;

System.Console.WriteLine( “This should never be executed” );

Exceptions 97

}

catch

{

System.Console.WriteLine( “The exception was caught!” );

}

System.Console.WriteLine( “Execution continuing normally...” );

}

}

Now the exception jumps right from the assignment line to the catch block. Once the catch block is done executing, execution continues on normally. The system has handled the exception, and knows that the problem doesn’t exist anymore. At least it hopes so. It’s really up to you to actually fix the problem inside the catch block.

The Finally Block

There are going to be times when you don’t want code jumping around all over the place when an exception is thrown. For example, say you have a file that you’ve opened up, and in the process of working on that file, something bad happens and an exception is thrown. Instead of having execution immediately jump out, maybe you would like to make sure the file was properly closed first.

Well, then you’d do this:

try

{

//open a file here

...

//some exception may be thrown here

...

//other code

}

catch

{

// handle error here

}

finally

{

// close the file here. This code will always execute

}

If you put the file-closing code inside the try block, then there’s a chance it might not execute. If you put it in a catch block, then it will only execute when an exception is thrown.

98Chapter 5 One More C# Chapter

You could, of course, put the same code in both a try block and a catch block, but that’s code duplication, and code duplication is bad. Instead, put it in the finally block, thereby guaranteeing that the code will always be executed after the other blocks, no matter what happens.

n o t e

You should note that the scope for all three blocks is different. Anything you define inside a try block is not accessible outside of that block, so by the time you get to a catch block or finally block, everything declared inside the try block has gone out of scope and cannot be accessed. Therefore, in the previous example, you should declare (but not open) the file object outside of the try block.

Advanced Exception Topics

Exceptions are actually pretty advanced, and there’s a lot to them. An exception is actually an object. Every time an exception is thrown, the system creates an exception object that contains information about what went wrong. All exceptions inherit from a common base class: System.Exception.

For example, in Examples 5.1 and 5.2, the exception that was thrown was of type System.IndexOutOfRangeException. That exception type tells you that you tried using an index that was out of range on a collection object.

Catching Specific Exceptions

Sometimes more than one error occurs in your code. Using a plain catch block catches all exceptions and assumes that you handled them; then it continues executing. But this is not always a good idea, and let me explain why. You are usually going to know what errors to expect in some code. When dealing with files, you know you’re going to face the possibility that you’re going to run out of space. But there’s also the possibility that something completely unexpected will happen. A catch block is sort of like a contract. It says, “I’m going to handle this exception, and then the program is going to be able to keep running just fine after this.” But if something completely unexpected happens, how can you be sure the problem is fixed? You can’t!

So C# allows you to catch exceptions of specific types. Check it out:

try

{

// try some code that may throw an index error

}

catch( System.IndexOutOfRangeException e )

{

Exceptions 99

// print the error message:

System.Console.WriteLine( e.Message );

}

Now you’ve just made your code able to catch index exceptions. All other exceptions will be ignored, and execution will keep jumping upward until it finds a catch block that can actually handle it.

You can also chain catch blocks, like this:

try

{

// try some code that may throw an index error

}

catch( System.IndexOutOfRangeException e )

{

// handle index error

}

catch( System.OutOfMemoryException e )

{

// handle memory error

}

Each block will handle each error differently.

You may have noticed that in the previous examples I had a variable named e. That represents the actual exception object. Exceptions can store data specific to the error, in case you need to find out exact details of what happened.

Re-throwing Exceptions

There are going to be times when you want to catch an exception, but you won’t be able to handle it. These situations usually arise when you only want to know that an exception has been thrown (and not actually fix the problem) so that you can log it somewhere or notify something. In that case, as you haven’t handled the exception, you want to re-throw it. This is a very simple task:

catch

{

// do some notification here

throw;

// rethrow it

}

The exception has been re-thrown now, and will keep jumping up until another catch block catches it.