After doing some more prototyping, I started to realize that my test level had some serious pacing and playability issues. The most important issue was that flipping switches or standing on pressure plates would sometimes cause things to occur offscreen, which made it difficult to understand what was going on in a puzzle.

To try and solve this problem, I implemented a basic system for automatic camera control. When an object in the game’s state changes, it asks the Game object to make sure the player notices, by calling CameraLookAt and passing in its own position.

Every frame the game runs an UpdateCamera function that scans through all the CameraLookAt requests from the previous frame, and uses a simple algorithm to determine whether it should do anything to the camera, and if so, what point it should center the camera on:

  • At least one of the CameraLookAt requests must be for a point that is not currently on-screen.
  • If any of the requests is for a point that is on-screen, use all of the requests to determine the point to center the camera on.
  • Compute a bounding rectangle that contains all the CameraLookAt requests, and center the camera on the center of that rectangle.

One slight problem this algorithm has is that it only operates on points, not areas of interest. For now, I solve this by excluding 10% of the screen on all sides, assuming that the player is unlikely to notice things changing on the edges of the screen. In the future, I’ll probably change the algorithm to operate on rectangles. The use of a bounding rectangle also means that if a lot of things happen at once, it won’t be able to show them all to the player. I think the only way to solve that problem is to control the camera manually, either with player inputs or with carefully scripted camera controls for each level. The latter is the approach I plan to use in the future.

The camera transitions are also a bit too abrupt. One future improvement will be to adjust the speed at which the camera moves, based on how far it has to move. It might also be good to try and move the camera in advance of changes in the level, so it’s easier for the player to figure out what happened. Since one-way barriers transition instantly it can be a little hard to realize why the camera is looking at them.

protected void UpdateCamera () {
    float screenWidth = GraphicsDevice.Viewport.Width;
    float screenHeight = GraphicsDevice.Viewport.Height;

    if (CameraLookAtQueue.Count > 0) {
        var cameraTopLeft = new Vector2(ViewportPosition.X + (screenWidth * 0.1f), ViewportPosition.Y + (screenHeight * 0.1f));
        var cameraBottomRight = new Vector2(ViewportPosition.X + (screenWidth * 0.9f), ViewportPosition.Y + (screenHeight * 0.9f));

        var minPos = new Vector2(float.MaxValue, float.MaxValue);
        var maxPos = new Vector2(float.MinValue, float.MinValue);
        float duration = 1.0f;
        int offscreenCount = CameraLookAtQueue.Count;

        for (int i = 0; i < CameraLookAtQueue.Count; i++) {
            var la = CameraLookAtQueue[i];

            duration = Math.Max(la.Duration, duration);
            minPos.X = Math.Min(minPos.X, la.Position.X);
            minPos.Y = Math.Min(minPos.Y, la.Position.Y);
            maxPos.X = Math.Max(maxPos.X, la.Position.X);
            maxPos.Y = Math.Max(maxPos.Y, la.Position.Y);

            if ((la.Position.X >= cameraTopLeft.X) &&
                (la.Position.Y >= cameraTopLeft.Y) &&
                (la.Position.X <= cameraBottomRight.X) &&
                (la.Position.Y <= cameraBottomRight.Y))
                offscreenCount -= 1;
        }

        var center = minPos + ((maxPos - minPos) / 2.0f);

        if ((center.X >= cameraTopLeft.X) &&
            (center.Y >= cameraTopLeft.Y) &&
            (center.X <= cameraBottomRight.X) &&
            (center.Y <= cameraBottomRight.Y) &&
            (offscreenCount == 0)) {
            // If all the points are on screen, don't move the camera.
        } else {
            CameraStack.Add(new LookAtCameraController(
                center, duration
            ));
        }

            CameraLookAtQueue.Clear();
        }

        Vector2 pos = new Vector2();

        for (int i = 0; i < CameraStack.Count; i++) {
        var cc = CameraStack[i];
        var newPos = cc.GetCameraPosition(screenWidth, screenHeight);
        var w = cc.GetWeight();

        if (w <= 0) {
            CameraStack.RemoveAt(i);
            i -= 1;
        } else {
            pos = new Vector2(pos.X + ((newPos.X - pos.X) * w), pos.Y + ((newPos.Y - pos.Y) * w));
        }
    }

    ViewportPosition = pos;
}

Solving the visibility problem with the camera made my test level’s puzzles a lot easier to follow.

The pacing issues are more fundamental, unfortunately. My test level has a lot of boring, empty spaces to run through. One thing that will help is adding more content to the level, like enemies to fight in the empty hallways, deadly traps to avoid, etc. But even then, having to trek through lots of areas repeatedly to solve puzzles sucks, so I’m going to have to pay careful attention to this when designing and testing levels. I haven’t come up with a good strategy for making the pacing feel ‘right’ yet.

I also spent some time doing some performance tuning using the CLR Profiler and dotTrace, for memory and CPU profiling respectively – I had reached the point where my game no longer ran at 60FPS on the XBox 360, though it ran fine on my PC, which is kind of to be expected – I hadn’t done much optimization at all up until this point.

Other than basic things like tuning code in hotspots to be more efficient, I also spent a little time trying to control my allocation rate. On the 360, if you allocate temporary objects rapidly enough, the garbage collector will have to interrupt you often, causing your game to stutter. I had to hunt down a few places in my game code where I was accidentally creating temporary instances of things like delegates and boxed value types.

In one case, the compiler had made a clever optimization by moving a new statement further up in the function for performance reasons, without taking into account that it caused a function to always allocate a temporary object instead of allocating one sometimes. At first I thought this was a compiler bug, until I looked at the disassembly and realized that the only impact of the optimization was allocation – no extra code was being run, so it only had GC impact. Regardless, it was kind of unexpected – I was unable to figure it out until I started looking at disassembly in Reflector. Having CLR Profiler‘s call/allocation tree helped a lot in knowing where to look, at least.

I spent a little time tuning my collision detection and motion code, by removing unnecessary calculations, adjusting caching logic, and so on, but I’m also nearing the point where I’ll need to start optimizing my collision detection algorithms instead of just making the code faster. Right now it’s entirely brute-force, but in the future I’ll need to build some sort of a scene graph or quadtree to store my collision geometry, so I don’t have to test the player against the entire level every frame. Right now my CPU usage goes down by about 5-10% any time the player is jumping/falling, just because I perform less collision checks when he’s in that state. The ComputeStandingY algorithm is also kind of a problem here, since as currently implemented it has to walk the entire level 5 times every frame.

The CPU profile wasn’t particularly enlightening – most of the bottlenecks were exactly what I figured they would be – all the looping and floating point calcluations in my motion and collision detection code. However, there were a few things I hadn’t expected to show up:

  • Actual CPU time (3-4% of total) was being spent in the constructors for Rectangle, Size, and Vector2 in my drawing routines. Totally unnecessary; most of it was a result of using ‘foo = new Vector2(x, y);‘ when I could have used ‘foo.X = x; foo.Y = y;‘ instead. I’m not quite sure why the compiler wasn’t inling these calls, though.
  • Actual CPU time (another 4-5% of total) was being spent looping over all of a level’s tilesets every frame to map a tile index to a texture and rectangle. This was stupid and extremely easy to solve – I just moved that looping operation into a function and cached the result. I could speed it up further by doing all of those lookups once when loading the level, eliminating the need for the caching logic, but it’s so low on profiles now that it doesn’t really matter.
  • The GetPolygonAxes function in my collision code had an optimization in it that turned out to be a pessimization: It did some work to avoid adding the same axis to the list twice, so that the collision detection code wouldn’t have to check it twice. On paper, this is a significant performance improvement, since it reduces a rectangle vs rectangle check from 8 iterations to 4. In practice, it actually made my performance worse, because performing an equality check between Vector2 objects has tangible overhead, as does walking over the current contents of the list. I might be able to bring it back using some sort of clever hashing technique, but for now I ended up removing that “optimization” entirely.

Here’s some footage of the camera controls in action, along with more of the test level. I edited out the boring walking sections with a sledgehammer since nobody would want to watch them – hope the cuts aren’t too jarring.