TSANCHEZ'S BLOG

Postmortem: Dreamworks Superstar Kartz

Well... this took me long enough to publish :|

A Different Start

Dreamworks Superstar Karts started out as a far different beast. High Impact Games was initially tasked with the creation of a dKart game for a more memorable mascott. In what would have been a reboot of an older franchise, that was later canceled We started out the game with a single level to demo all the different abilities we wanted in the final product.
  • Branching paths, so everyone isn't always grouped up.
  • Powerup Crates, with various random weapons.
  • A modular Kart system.
  • Various terrain types, which could be navigated or hazzards depending on your Kart's modules
  • Various terrain hazzards which could be triggered.
  • Speed boosters.
  • Lots of air time, for doing tricks.
In that single level we crafted:
  • 4 playable characters (to be expanded to 16) each with their own abiliites
  • Modular car parts for moving around the track
  • Item pickups
  • 16 player multiplayer
  • Support for PS3, X360 and PC.

Engine Revamp

This production was a total revamp of our previous engine. While there was a lot of shared code, lead programmer Ryan Jucket took it upon himself to update our engine. The major overhaul was the replacement of our "Moby" system which was largely driven by code, to a Entity Component System driven by data. There was also a lot of work done to create points where we'd be able to add more multi-threading to the game in order to take advantage of X360's and PS3's multiple cores. When I was brought into the project there were other items to overhaul as well. I was tasked with updating the particle sytem to be data-driven, based off scripting used in the game Spyborgs. I also was tasked with updating much of the games collision handling logic with a fully fledged Physics engine.

Entity Component System

This was an amazing step forward from our previous engine design. The biggest contribution to the code was purely from the re-use that it forced onto the game programmers. The amount of specifics written dropped dramatically as components were used to carve out whole classes of gameplay interaction. The initial demo of the game resulted in 29 gameplay components backed by 30 engine components.

The engine broke up all objects into a collection of components, which were named in one of the core configuration files.

EntityTypeList_Tuning entityTypeList =
{
  types = {
    {
      name = "Kart"
      components = { 
        "ChildManager",
        "SoundEmitter",
        "Physics",
        "Damageable",
        "Kart",
        "KartInventory",
        "Animation",
        "Mesh",
        "CamTrackable",
        "NavLocator",
        "NavPlanner"
      }
    }
    {
      name = "OilSlick"
      components = { "OilSlick" }
    }
    {
      name = "PickUp"
      components = {
        "SoundEmitter",
        "Mesh",
        "Physics",
        "PickUp",
        "ChildManager"
      }
    }
    // etc...
  }
}
Obviously, there were some components that contained a lot of very specific code, eg. "OilSlick". But there were quite a few components like "Mesh" that were driven through configuration files, allowing for the same component to wear many different skins. The art pieces themselves could similarly contain sub-objects in the Maya files. A "ChildManager" component would allow the Entity to traverse the art and automagically spawn the attached objects. This gaves artists a lot of control they previously didn't have. They could now place effects, as well as see where art pieces in our modular Karts would get placed.

Along with this ECS, came a simple text serialization tool. The tool could process a subset of the C++ language to discover the memory layout of structures. We created special header files that contained only structure definitions. These became the schema files which defined our configuration files. The configuration files (like the EntityTypeList_Tuning above) could then be written out in text. The associated build tool would turn the textual data into binary data that matched the schema structure, so no parsing would be needed at runtime.

Since we went with a more data-driven approach in this engine, several of the systems were re-designed to have a debug re-loader loop. In debug mode, the game would look out for files that have been updated, and would re-load the associated assets. This made some editing (textures, and config files mostly) very interactive.

I personally used this config language and texture-reloading to create a new particle configuration system. The particle system itself was still hard-coded, and provided only a few specific types of emitter. Each emitter came with a specific set of configuration parameters, that let artists (or myself) create effects.

// Radial fire colored smoke
EmitterTune exp_med_smokeConfigRadial = {
  material = {
    blend = BLEND_ADD;
    textureSig = "SFX/TEXTURES/smoke0.htx";
    layer = 19;
  }
  orientation = PART_ORIENT_CAM;

  emitShape = EMITTER_SHAPE_RADIAL_DISK;
  volumeInnerRadius = 0.0;
  volumeOuterRadius = 4.0;

  orientAngleZ = 90.0

  burstCount = 7.0;
  emitRate = 4.0;

  systemConstants = {
    randomRotation = true
    life  = { min = 0.75 max = 1.0 };
    size  = { min = 1.5 max = 2.0 };
    rotSpeed = { min = -45.0 max = -90.0 };
    linSpeed = { min = 1.0 max = 2.0 };

    gravity = 4.0;
    linDamping = 0.00;

    scaleStart  = 2.0
    scaleEnd  = 3.0

    colorRamp = {
      { t = 0.0, color = { r = 255 g = 192 b = 0 a = 0 } },
      { t = 0.5, color = { r = 117 g = 50 b = 22 a = 255 } },
      { t = 1.0, color = { r = 60 g = 25 b = 12 a = 0 } },
    }
  }
}
Since the configuration language supported a type of inheritence, it was possible to insert previously defined structures as the values in later strutures. By storing many of the "parts" of an effect in a common configuration, artists could re-use them in later effect bundles. These effect bundles could in turn include things other than just the particles themselves. Lights, sounds, and physics forces could be added to any effect file.
EffectsScript entCmp_EffectsScript_Tuning = {
  parEvents = {
    { time = 0.0, lifetime = 1.5, mainSys = exp_med_smokeConfigRadial }
    { time = 0.0, lifetime = 0.3, mainSys = exp_med_emberConfig }
  }
  lightEvents = {
    { time = 0.0, lifetime = 0.1f, mainSys = exp_med_light_flash_Config },
  }
  explodeEvents = {
    { time = 0.06, radius = { min = 16.5, max = 20.0 }, impulse = 30.0,
    randImpulseMult_Min = 0.4,
    randImpulseMult_Max = 1.0}
  }
  soundEvents = {
    { time = 0.0, name = "Play_Explode" }
  }
}

Threading

Lead Engine programmer Jon Mayfield also took it upon himself to create a "Job" system for the new engine. This was effectively a thread pool that could be used to launch various trivially parallel tasks across the many cores of the X360. The awesome part about the ECS changes was that it allowed us to break the scene down into many trivially parallel operations, as many components could all be updated at once.
template<typename T>
s32 JobFun_RegularComponentUpdateJob(JobData *pJobData) {
  JobData_ComponentUpdate<T> *pData = static_cast<JobData_ComponentUpdate<T>*>(pJobData);
  pData->m_pCmp->Update(pData->m_deltaTime);
  return 0;
}

// ...

void UpdateItems(f32 dt) {
  JobMmanager_BeginAddJobs();
  // update Flocks
  for (tEntCmpMgr_Flock::tIterator iCmp = g_entCmpMgr_Flock.GetBegin();
       iCmp != g_entCmpMgr_Flock.GetEnd();
       ++iCmp) {
    JobData_ComponentUpdate<EntCmp_Flock> data;
    data.m_pCmp = &*iCmp;
    data.m_deltaTime = dt;
    JM_AddJob(
        &JobFun_RegularComponentUpdateJob<EntCmp_Flock>,
        &data,
        TYPE_ENTITY_UPDATE,
        NULL);
  }
  JM_EndAddJobs();
  // etc
}
This nicely handled the fine-grained threading of systems. Most the components, animations, and skin morphing were handled on these fine-grained thread. However, there was more threading going on. We also added some more high-level threading to the engine as well. The graphics state was double-buffered away from the gameplay state. This allowed the graphics to run parallel, and one frame behind the gameplay updates.

Physics

For the physics system, I incorporated Bullet Physics as the physics library. This ended up being a rather decent solution, as it did localize all the code and data for physics handling. In our previous game titles, physics was handeled on a per-object basis, by that object itself. This could result in some strange interactions that would generally allow some things to push other things through the world. Bullet solved some of this, though it brought a whole slew of other problems to development.
  • It ate a lot of memory.
  • It was, at the time, mostly single threaded.
  • It was very slow for some platforms (3ds and Wii)
  • The material system was lacking some of the depth we needed.
  • The object-filters was lacking some of the depth we needed.
  • The collision response code wasn't exposed well, making it difficult to add and sounds/effects.
Overall though, I think it ended up being the right choice to go with, as it provided a lot of the ground work that would be needed to keep up with other "next gen" titles.

While some of these issues were just technical problems that anyone needs to solve, I'll go into some depth with the more game-specific collision response issues. In a physics simulation, you don't usually need anything out except the matricies of the objects. But in a game physics simulation you often need to know a lot of information about the objects that are collidiing:

  • Which objects are colliding?
  • How hard did they collide?
  • Which objects should physically interact?
  • What type of material is the colliding surface made of?
The which-objects part is simple. You can check the bullet contact points, and take every unique pair of contact objects as an interaction that needs to be handeled.
void Physics_Callback(btDynamicsWorld *world, btScalar timeStep) {
  g_physics_manifoldCache.clear();

  btCollisionWorld *pCollisionWorld = static_cast<btDiscreteDynamicsWorld*>(world)->getCollisionWorld();
  const int numManifolds = world->getDispatcher()->getNumManifolds();
  for (int i = 0; i < numManifolds; i++) {
    btPersistentManifold* contactManifold = pCollisionWorld->getDispatcher()->getManifoldByIndexInternal(i);
    btCollisionObject* colObj0 = static_cast<btCollisionObject*>(contactManifold->getBody0());
    btCollisionObject* colObj1 = static_cast<btCollisionObject*>(contactManifold->getBody1());

    CacheManifold( colObj0, contactManifold );
    CacheManifold( colObj1, contactManifold );
  }
}
From there it's possible to parse the Manifolds to get the normal, hit position, and force from the manifold.
  for (int p = 0; p < pManifold->getNumContacts(); p++) {
    const btManifoldPoint&pt = pManifold->getContactPoint(p);

    btCollisionWorld::LocalShapeInfo shapeInfo;
    if( !swapped ) {
      shapeInfo.m_triangleIndex = pt.m_index1;
      shapeInfo.m_shapePart = pt.m_partId1;
    } else {
      shapeInfo.m_triangleIndex = pt.m_index0;
      shapeInfo.m_shapePart = pt.m_partId0;
    }

    // best point?
    if (pt.getDistance() < 0.0) {
      const btVector3 btHitNormal = pt.m_normalWorldOnB * directionSign*(-1);
      const btVector3 btHitPos = (swapped)
          ? (pt.getPositionWorldOnB())
          : (pt.getPositionWorldOnA());
      const VEC3 hitNormal = { btHitNormal.x(), btHitNormal.y(), btHitNormal.z() };
      const VEC3 hitPos = { btHitPos.x(), btHitPos.y(), btHitPos.z() };
      
      // process
    }
As you can see, it's also possible to get the exact triangle on a triangle mesh collision. Which rounds out the last concerns, as the two colliding game objects will each contain some information about what they are made of. In the case of collision shapes, like a sphere, the whole sphere was considered a single material. In the case of triangle meshes, we stored a second datastructure that listed the material of every triangle on the collision mesh. These materials could be used to modify how a collision interaction was going to take place, and to pick the effects/sounds that will play.

Once we had all of this surface information we could use it for one last thing: Driving Surfaces. The game featured several wheel types that interacted differently with each of the surfaces described in the world mesh. Fish tires would let you float on water, a normally deadly surface for other karts. Sand could slow you down if you hit it, but Crab tires would avoid the slowdown. But the most interesting surface was the sticky surfaces. Octopus tires let the Karts change which direction they thought was "down". They'd follow along the surface normal of the road underneath them, allowing the Kart to climb walls and even drive up-side-down. Each of actions was of course greeted with sounds, particles, and animations to show off the presence of the surface types.

A Game Reborn

This prototype game was eventually scrapped. The publishers wanted to go a different route. While I went on to work on Phineas and Ferb: Across the second Dimension, High Impact continued to shop around the Kart game idea. Eventually Dreamworks picked up the tab, and we set to work with a new theme. Many of the mechanics stayed the same, but we still scrapped a lot of work. Many of the weapons were dropped, and replaced with more traditional (read mariokart) weapons. The various driving surfaces were scrapped. And most the morph-animation cartoon effects were removed. The platform support list changed as well, with us shifting to PS3, X360, Wii, and 3DS.

The art was swapped out, and most the gameplay code remained the same. A few new weapon changes were made, as well as tweaks to the 'trick' mechanics. The biggest code changes however came from the new set of platforms being supported. The PS3 and X360 performed quite similarly, and our game didn't push either platform to the limit. However the Wii was significantly less powerful. Even with significant optimization work, the Wii suffered a quality hit. Textures, art, and animations were made less detailed. Particle effects were scaled down, and most of the physics breakables were removed.

The 3DS however suffered the most. It was a new platform at the time. Our initial devkits were nothing more than an unprotected motherboard with a plug-in joystick controller. The compiler was crappy, and the debugger next to useless. Nothing we had came close to being optimized for 3DS, and much had to be redone. The entier UI was re-done from ScaleForm flash to specific hand-coded huds for 3DS. The art was once again scaled back, with the active racer count taking a significant hit. Nearly all the physics objects were removed from the levels, and only essential particle effects were left in the game.

I ended up leaving the project before it was finished. The PS3 and X360 versions were nearly complete at the time, with only the 3DS and Wii pushing towards final release.

Copyright © 2002-2019 Travis Sanchez. All rights reserved.