官术网_书友最值得收藏!

Organizing the code by what it does, not what it is

What is the difference between the Raider and the Bomber, really? How are a Raider and a SuperRaider different? Maybe they have a different speed, a different texture, and a different damage value? Do these changes in data really require a new class? Those are really just different values, not different behaviors. The problem is that we are creating extra classes because the concept of a Raider and SuperRaider is different, but there aren't differences in behavior.

Our class hierarchy actually violates three principles I teach, two of which I learned from the Gang of Four book:

"Keep your inheritance trees shallow"

"Favor object composition over class inheritance" - Gang of Four, p20

"Consider what should be variable in your design. This approach is the opposite of focusing on the cause of redesign. Instead of considering what might force a change to a design, consider what you want to be able to change without redesign. The focus here is on encapsulating the concept that varies, a theme of many design patterns" - Gang of Four, p29

A different way to state the third principle is the following:

"Find what varies and encapsulate it"

These principles exist to eliminate, or completely avoid, the problems that can and will arise when using inheritance.

The problem with our current design is that if we create a new class for every object type, we will end up with a lot of little classes that are mostly the same. Raider, SuperRaider, Bomber, and SuperBomber are mostly the same with just a few minor differences, some of which are only differences in float and int values. While this approach may seem like an improvement over the easy way, it becomes a problem because we will end up writing the same behavior code over and over again in many classes. If we have a lot of enemies, we might end up writing the same basic ChasePlayerAI code in every Update function. The only solution is moving the ChasePlayerAI up to a base class.

Let's take another look at our Space Shooter hierarchy but this time, let's add in some different behaviors to our classes:

Figure 3.3 - After adding behavior to our objects (refer graphic bundle)

We have decided that our base object class will at least be drawable to make things simple. If an object such as a trigger region needs to be invisible, we can simply support disabling rendering by putting a bool in the drawable behavior so it won't get drawn. However, with this game object approach, I still have some duplicated code. Both the Raider class and the AttackStation class have some AI that targets and shoots bullets at the Player. We have only duplicated our code once so maybe it isn't a big deal.

Unfortunately, all game designs will change. What happens when our designer wants to add asteroids to our game? Technically, they are structures so they need some of the data inherited from that class, but they also move. Our designer also really liked the SpawnerStation class and wants to add that ability to a new SpawnerPlanet class, and to a new BossSpawner class. Should we rewrite the code two more times, or refactor the code into the base class? Our designer also wants to give the Station the ability to slowly patrol an area. This means the Station class needs the Patrol AI ability as well. Let's take a look at our hierarchy now:

Figure 3 4 - After refactoring duplicate code to our base class (refer graphic bundle)

As it turns out, this approach isn't as flexible as it originally seemed. In order for our design to be really flexible, almost all of the behaviors need to be factored up into the base class. In the end, we aren't much better off than when we wrote our game object the easy way. And it is still possible that our designer will want to create the RepairHelper that chases the Player, meaning that everything will be in the base class.

This might sound like a contrived example but remember that games take years to develop and are likely to change. DMA Design's Grand Theft Auto was originally titled Race'n'Chase, but it was changed because a bug caused the police to try and run the Player off the road instead of pull them over. This ended up being way more fun. Another example is Blizzard's first-person shooter Overwatch, which was originally in development for 7 years as a massively multiplayer online game.

The purpose of object-oriented programming is to recognize that designs will change and to write code with that change in mind.

Another problem with our inheritance approach is that it isn't very easy to add or remove abilities at runtime. Let's say our game has a special power-up item that will let the Player use a shield for 1 minute. The shield will absorb 50% of the damage done to the Player for 1 minute then remove itself. We now have the problem of making sure that when a bullet collides with the shield, it will transfer some of the damage to the Player. The shield isn't just responsible for itself; it is responsible for the Player object too.

This same situation exists for all things that will affect another game object for some duration of time. Imagine if we want our Raider to be able to do acid damage to the Player over 5 seconds instead. We need a way to attach this acid damage to the Player, and to remember to remove it after 5 seconds. We could add new variables such as bool hasAcid and float acidTime in the Player class that we can use to know whether we should do acid damage in this frame. However, this still isn't a flexible solution, because each new type of damage caused over time will need new variables like this.

In addition, there is no way to stack the acid damage effect if three enemies are attacking the Player with acid damage. If we like this ability and want the Player to use it, we also need to give all game objects these extra time-based damage variables and behavior code. What we would really like to do is attach acid behavior (or any effect) onto a game object at runtime and have it automatically detach itself when the effect is over. We are going to talk about how to do that later in this chapter, but first we need to talk about one more problem related to inherence hierarchies in C++.

主站蜘蛛池模板: 湟源县| 石棉县| 秦皇岛市| 平湖市| 沙坪坝区| 巨鹿县| 威信县| 平泉县| 浮山县| 闸北区| 隆昌县| 三江| 木里| 洛浦县| 琼海市| 巴东县| 当涂县| 绥芬河市| 呼伦贝尔市| 仁寿县| 蒙阴县| 沛县| 财经| 沧州市| 兴仁县| 德保县| 哈尔滨市| 大厂| 泰安市| 土默特左旗| 汽车| 三门峡市| 沁源县| 田林县| 都安| 郎溪县| 南江县| 启东市| 海南省| 贵溪市| 北流市|