TDD And Game Physics
I'm playing around with a small game project and as I'm not very experienced in TDD I'd love to get some expert opinions on a couple of things.
First of all, I realized early on that TDD did not seem ideal for game development. It seems that opinions vary pretty wildly on the subject. My initial uneducated opinion was that TDD seemed as though it would work very well for all the game logic. I thought to myself that anything that would deal with video output and sound would be abstracted into classes that would be tested visually.
Things started off well. The goal was to create a 2d space flight game (asteroids for those that care). I created a series of unit tests for the Ship class. Things like initialization, rotation, can easily be tested in a series such as: GetRotation(), TurnRotateRightOn(), Update(1), GetRotation(), Expect_NE(rotation1, rotation2). Then I hit the first problem.
My understanding of TDD is that you should write the test how you think you should use the class. I want the ship to be able to move so I wrote a class that basically said. GetCoordinates(), ThrustOn(), Update(1), GetCoordinates(). That was fine to make sure the ship moved somewhere. However, I quickly realized I had to make sure the ship was moving in the correct direction and at the correct speed. What followed was a 75 line unit test where I basically had to initialize the rotation, check the coordinates, initialize thrust, update the ship, get the new coordinates, check the new rotation. What's more, I can see no need to ever get the ship's velocity in the game (just the coordinates, the ship should update itself). Because of this I had no direct way of getting the velocity. So the test basically had to recalculate what the velocity should've been just so I could make sure it matched up with what coordinates I was getting after the update. All in all this was very messy, and very time consuming, but worked. The te开发者_如何学JAVAst failed, made the test pass, etc.
This was fine until later when I realized I wanted to refactor the update code of the ship into an abstract "Actor" class. I realized that while every subclass of Actor would need to be able to correctly calculate a new position, not every subclass would necessarily update their velocity the same way (some collide, some don't, some have static velocities). Now, I'm basically faced with the prospect of duplicating and altering that huge and massive test code and I can't help but think there should be a better way.
Does anyone have any experience in dealing with unit testing this sort of complex black box type of workings? It seems like I'm basically having to write the exact same physics code in the test just so I know what the result is supposed to be. It seems self defeating really, and I'm sure I'm missing the point of all this somewhere along the way. I'd greatly appreciate any help or advice that anyone could offer.
I suggest that you start by creating a component that computes position and orientation given a sequence of control inputs. That component then constitutes a "unit" for the purpose of testing. The test cases for that component would exercise all of the scenarios that you can think of: zero acceleration, constant non-zero acceleration, pulsed acceleration commands, etc. If the application has no need for velocity, then the component will not expose any functionality related to velocity.
When generating the expected outputs for inclusion in the tests, it is important to have high confidence that those expected results are correct. For this reason, one needs to minimize the amount of code required to generate the expected results. In particular, if you find yourself writing test scaffolding that is nearly as complex as the component under test, then the prospect of bugs appearing the tests themselves becomes a serious concern.
In this case, I would generate the test data directly from the equations of motion. I use Mathematica for this purpose as I can enter the equations directly, solve them and then generate graphs and tables of the results. The graphs let me visualize the results and thereby have confidence that they are credibly correct. Excel / OpenOffice / Google Apps could be used for the same purpose, as well as open source alternatives to Mathematica like Sage. Whatever one chooses, the key concern is to be able to solve the equations of motion without having to write non-trivial code.
Once we have a good set of test cases along with the expected results, we can code up the unit test. Note that the test code is very simple, performing no calculations itself. It simply compares the component's output to the hard-coded results that we obtained earlier. With the test cases in place, we can write the component itself, adding code until the tests all pass. Of course, in strict TDD these actions happen in exactly this order. I confess that I don't personally stick to the waterfall and tend to bounce back and forth between creating test data, writing tests and writing component code.
The Mathematica or Excel documents themselves have a useful life beyond the initial creation of the tests. They can be used again when new functionality is added or (heaven forbid) should bugs be found later. I would advocate treating the documents like source code.
At the end of this exercise, the result is a "bomb-proof" component that we have convinced ourselves will calculate the correct position and orientation for an object under any given set of control inputs. From this foundation, we can build further components that use that functionality, like ships, asteroids, saucers and shots. In order to avoid a combinatorial explosion of test cases for each component, I would depart from a strict black-box testing approach. So, for example, if we were testing a "ship" component, we would write tests knowing that it uses the position/orientation component. Using this white-box knowledge, we can avoid retesting all of the corner cases related to motion. The ship unit tests can perform "smoke tests" that verify that ships do, in fact, respond to control inputs but the main focus would be on testing any functionality unique to the ship component itself.
So, to summarize:
- make position/orientation calculation the responsibility of a single component
- generate the data for a comprehensive set of test cases using an external tool that gives high confidence that the data are correct
- avoid complex calculation logic in the tests themselves
- dodge the combinatorial explosion of test cases in a component hierarchy by using white-box knowledge
You could dumb down your tests a bit. Instead of checking for the correct vector and acceleration, you could simply check, that the object under test moves at all. In most games you would introduce some small amount of randomness into the physics model anyway to keep things interesting.
I think it might help if you took a more "episodic" approach. Rather than tracking coordinates and rotation across a continuum, which you seem to be doing, you could simply specify where your space ship should be, and at what rotation, after 30 game-steps. If that proves correct, then your ship probably did all the correct things in between as well. You'll write a lot less test code that way.
The same idea works by targeting the "episode" of a collision. If a billiard ball is meant to bounce off two walls and hit another ball, you could just raise an event when the final collision occurs, and check the "angle of incidence" of the collision. Once again you are not checking all the steps in between. If the angle of incidence is correct, then the ball probably bounced off the two walls correctly before hitting the final ball.
Of course you must be prepared for the case where no collision ever occurs. Your test can account for game-clicks per unit time to reach the final collision. You make the test run for the necessary number of game clicks to achieve the collision. If the collision has not happened within the prescribed number of clicks, then the test can properly fail.
All of this is done in game-clicks rather than real-time, so that the test can happen near instantly, rather than waiting for the expected result (as you would normally do if you were actually playing the game).
精彩评论