Running with Code Like with scissors, only more dangerous

18Nov/150

Exploring .dbc files with Dynamic Code Generation, part 3: Dynamic codegen!

Posted by Rob Paveza

Last time, we talked about how to populate a class based on its ordered fields, using Reflection to do the heavy lifting for us. This time, we're going to generate a DynamicMethod which runs at runtime in the same way that ConvertSlow did last time. Let's look at that this time.

First, let's change GetAt here:

        public T GetAt(int index)
        {
            if (_store == null)
                throw new ObjectDisposedException("DbcTable");

            _store.Seek(_perRecord * index + _headerLength, SeekOrigin.Begin);

            T target = new T();
            // This is the only change
            Convert.Value(_reader, _recordLength, this, target);
            return target;
        }

The only change here is that we've changed ConvertSlow to Convert.Value. The Convert field is a Lazy<DbcReaderProducer>; DbcReaderProducer is a delegate type that corresponds to the same signature as the ConvertSlow method:

internal delegate void DbcReaderProducer<T>(BinaryReader reader, int fieldsPerRecord, DbcTable table, T target)
        where T : class;

We're going to aim for creating DynamicMethods that have this signature. The Convert field then gets defined as follows:

        private static Lazy<DbcReaderProducer<T>> Convert = new Lazy<DbcReaderProducer<T>>(() =>
        {
            if (Config.ForceSlowMode)
                return DbcTable<T>.ConvertSlow;

            try
            {
                return DbcTableCompiler.Compile<T>();
            }
            catch
            {
                if (Debugger.IsAttached && !Config.ForceSlowMode)
                    Debugger.Break();

                return DbcTable<T>.ConvertSlow;
            }
        });

So the magic is all in the DbcTableCompiler. Let's take a look at how that will work. Before writing code, let's outline it, using the CharTitles.dbc file we wrote in Part 1:

public class CharacterTitleRecord
{
    [DbcRecordPosition(0)]
    public int ID;
    [DbcRecordPosition(1)]
    public int RequiredAchievementID;
    [DbcRecordPosition(2)]
    public string Title;
}

Were I writing a DbcReaderProducer<CharacterTitleRecord> by hand, it might look like this:

static void FillCTR(BinaryReader reader, int fieldsPerRecord, DbcTable table, CharacterTitleRecord target)
{
    // assumption: reader is already at the beginning of the record
    target.ID = reader.ReadInt32();
    target.RequiredAchievementID = reader.ReadInt32();
    int stringTablePos = reader.ReadInt32();
    target.Title = table.GetString(stringTablePos); // Note: Table.GetString restores the current Stream position
    reader.ReadInt32(); // unused column @ index 3
    reader.ReadInt32(); // unused column @ index 4
    reader.ReadInt32(); // unused column @ index 5
}

That's pretty straightforward, right? Let's break down how that compiles.

  • I have to call reader.ReadInt32() (or ReadSingle if it's a float-type parameter). That puts the return value on the evaluation stack.
  • If the field type is String, call table.GetString.
  • Store the value onto the target's field.

Before we go on, we need to talk about the .NET execution/evaluation stack. This execution stack is a virtual stack that doesn't necessarily have any relationship to the physical hardware; it's just conceptually how the CLR reasons about the data that it's processing. For example, suppose you have an assignment x = a + b;. The .NET stack would modify in the following ways:

  1. push a
  2. push b
  3. add (consumes the top two values on the stack, and pushes the result onto the stack)
  4. assign to 'x' (consumes the top value on the stack)

In this way, the stack remains balanced. You can read more about how the CLR runtime environment works here. The other thing to note is that the CLR follows a single calling convention. If a method being called is an instance method, then the instance reference is pushed onto the stack followed by the parameters in left-to-right order; if it is a static method, then the instance reference is skipped. (Variadic functions have their arguments converted to an array at the call site).

Knowing this, we should be able to compile our own implementation as long as we know the right IL opcodes. I do; let's compile the above function now.

static void FillCTR(BinaryReader reader, int fieldsPerRecord, DbcTable table, CharacterTitleRecord target)
{
    ldarg.3  // push 'target' onto the stack
    ldarg.0  // push 'reader' onto the stack
    callvirt (instance method) BinaryReader.ReadInt32(void) // consumes 'reader', pushes result
    stfld (instance field) CharacterTitleRecord.ID // consumes 'target' and result of previous, assigns value

    ldarg.3 
    ldarg.0
    callvirt (instance method) BinaryReader.ReadInt32(void)
    stfld (instance field) CharacterTitleRecord.RequiredAchievementID

    ldarg.3
    ldarg.2 // push 'table' onto the stack
    ldarg.0 
    callvirt (instance method) BinaryReader.ReadInt32(void)
    callvirt (instance method) DbcTable.GetString(int32) // consumes 'table' and the result of ReadInt32
    stfld (instance field) CharacterTitleRecord.Title 

    ldarg.0
    callvirt (instance method) BinaryReader.ReadInt32()
    pop // unused column @ index 3

    ldarg.0
    callvirt (instance method) BinaryReader.ReadInt32()
    pop // unused column @ index 4

    ldarg.0
    callvirt (instance method) BinaryReader.ReadInt32()
    pop // unused column @ index 5

    ret
}

Woohoo! Now THAT is the kind of thing that seems highly automatable. Let's start thinking about how we can implement this. Starting with the actual Compile method:

        internal static DbcReaderProducer<T> Compile<T>()
            where T : class
        {
            // Supposing <T> is "DbcReader.CharacterTitleRecord", this method 
            // creates a DynamicMethod named $DbcTable$DbcReader$CharacterTitleRecord, with the same access
            // to types as the DbcTableCompiler type (i.e., internal to this assembly.
            // It returns void, and accepts BinaryReader, int, DbcTable, and T as its arguments.  In other words, 
            // it is compatible with DbcReaderProducer<T>.
            DynamicMethod method = new DynamicMethod("$DbcTable$" + Regex.Replace(typeof(T).FullName, "\\W+", "$"), typeof(void), new Type[] { typeof(BinaryReader), typeof(int), typeof(DbcTable), typeof(T) }, typeof(DbcTableCompiler).Assembly.ManifestModule);
            ILGenerator gen = method.GetILGenerator();

            var properties = GetTargetInfoForType(typeof(T)); // Same method as before
            var propertyMap = properties.ToDictionary(ti => ti.Position);
            var maxPropertyIndex = propertyMap.Keys.Max(); // We go in order, unlike the naïve implementation
            for (int i = 0; i <= maxPropertyIndex; i++) 
            {
                TargetInfo info;
                if (propertyMap.TryGetValue(i, out info))
                {
                    EmitForField(info, gen); // We have to do our magic here.  Allow us to grow to support properties later.
                }
                else // unused column below
                {
                    gen.Emit(OpCodes.Ldarg_0);
                    gen.EmitCall(OpCodes.Callvirt, BinaryReader_ReadInt32, null);
                    gen.Emit(OpCodes.Pop);
                }
            }

            gen.Emit(OpCodes.Ret);

            return method.CreateDelegate(typeof(DbcReaderProducer<T>)) as DbcReaderProducer<T>;
        }

All told, that is absolutely not complex nor scary. EmitForField is also not terribly complex:

        private static void EmitForField(TargetInfo info, ILGenerator generator) 
        {
            Debug.Assert(info != null);
            Debug.Assert(generator != null);
            Debug.Assert(info.Field != null);

            generator.Emit(OpCodes.Ldarg_3);

            EmitTypeData(info, generator);

            generator.Emit(OpCodes.Stfld, info.Field);
        }

The EmitForField method is designed as it is to allow us to grow to support properties later. (That will require pushing a reference to the 'target' object before doing the type-specific mutations and then calling the property setter, which is a slightly different activity than this one). So let's look at EmitTypeData, which does the real heavy lifting:

        private static MethodInfo BinaryReader_ReadInt32 = typeof(BinaryReader).GetMethod("ReadInt32");
        private static MethodInfo BinaryReader_ReadSingle = typeof(BinaryReader).GetMethod("ReadSingle");
        private static MethodInfo DbcTable_GetString = typeof(DbcTable).GetMethod("GetString");

        private static void EmitTypeData(TargetInfo info, ILGenerator generator)
        {
            switch (info.Type)
            {
                case TargetType.Float32:
                    generator.Emit(OpCodes.Ldarg_0);
                    generator.EmitCall(OpCodes.Callvirt, BinaryReader_ReadSingle, null);
                    break;
                case TargetType.Int32:
                    generator.Emit(OpCodes.Ldarg_0);
                    generator.EmitCall(OpCodes.Callvirt, BinaryReader_ReadInt32, null);
                    break;
                case TargetType.String:
                    generator.Emit(OpCodes.Ldarg_2);
                    generator.Emit(OpCodes.Ldarg_0);
                    generator.EmitCall(OpCodes.Callvirt, BinaryReader_ReadInt32, null);
                    generator.EmitCall(OpCodes.Callvirt, DbcTable_GetString, null);
                    break;
                default:
                    throw new NotSupportedException("Invalid type for target property.");
            }
        }

Looks suspiciously similar to what we did before! In fact, we can now see each of the code paths, plus an extra code path for 32-bit floats. So that's pretty set to go.

What does this do for us? Iterating through the CharTitles table 5 times goes from 37ms to 17ms on my laptop when not plugged in; iterating through the ItemDisplayInfo table 5 times goes from 880ms to 240ms. That's not a bad gain, especially with no optimizations like seeking!

Next time: We're going to optimize string reads.

16Nov/150

Exploring .dbc files with C# Dynamic Code Generation, part 1: Defining the problem

Posted by Rob Paveza

You know, I look back at my blog after all these years and the particularly infrequent updates and reflect a little bit on just how much things have changed for me. I know that right now I want to be writing code using back-ticks because I'm so accustomed to writing Markdown. But that's neither here nor there.

I recently published a project called DbcExplorer on GitHub. This was just a pet project I'd been working on during the World of Warcraft: Mists of Pandaria timeline; I'd just joined Microsoft and had my Windows Phone, but there's no Mobile Armory on Windows Phone (or even Big Windows, for that matter). A little bit of background: World of Warcraft stores simple databases in files with an extension of .dbc or .db2; these databases allow rapid lookup by ID or simple fast enumeration. There are a myriad number of them, and they commonly change from version to version. The reason I wanted them was to be able to crawl item information and achievement information for the purpose of creating a miniature Mobile Armory for Windows Phone, that could at least tell you which achievements you were lacking, and people could vote on which achievements were easiest, so that you could quickly boost your score.

(Side note: When Warlords of Draenor was released, Blizzard changed their storage archive format from MPQ to CASC. Ladislav Zezula, who created StormLib, which was a C library for accessing MPQ files, had made some progress at the time at CASC as well. However, I couldn't get it to work at the time, so I stopped working on this project. Ladik and I recently figured out what the disconnect was, and I've now wrapped his CascLib into CascLibSharp, but I don't know that I'll be resurrecting the other project).

Anyway, DBC files are pretty easy. They have a header in the following form:

uint32        Magic 'WDBC', 'WDB2', or 'WCH2'
uint32        Number of records
uint32        Number of columns per record
uint32        Number of bytes per record (always 4x # of columns as far as I can tell)
uint32        String block length

The files that aren't of type 'WDBC' have a few additional fields, but the general structure is the same. The files then have the general form:

DbcHeader     Header
Record[Count] Records
uint8         0  (Start of string table, a 0-length string)
uint8[]       String block (UTF-8 encoded, null-terminated strings)

Each column is one of:

  • Int32
  • Float32
  • String (an int32-offset into the String Table)
  • "Flags" (a uint32 but usually has a fixed set of bit combinations)
  • Boolean (just 0 or 1)

So this pretty well defines the problem space. We need to support deserializing from this binary format into plain objects, so that I can say I have a DbcTable<T>, and my runtime will be able to enumerate the records in the table. Now, because the CLR doesn't guarantee how the properties on objects will be enumerated (at least to the best of my knowledge); it probably keeps a consistent order based on some ethereal thing, but I don't know what that order is based on, so before I go, I probably have to do something.

Briefly, let's look at DBFilesClient\CharTitles.dbc. This file (at least as of the most recent patch) has six columns. I don't know for sure, but it looks like the following:

Column    Type       Description
0         Int32      ID
1         Int32      Required achievement ID
2         String     Title
3         String     Title, repeated
4         Int32      Unknown, just seems to continuously increase
5         Int32      Reserved (all records have 0)

Since I don't know what to do with columns 3-5, I can just define the following class:

public class CharacterTitleRecord
{
    [DbcRecordPosition(0)]
    public int ID;
    [DbcRecordPosition(1)]
    public int RequiredAchievementID;
    [DbcRecordPosition(2)]
    public string Title;
}

Next time: We'll see how the naïve implementation deserializes each record.

26Aug/150

Star Wars Commander turns a year old, and Disney Fails

Posted by Rob Paveza

Wow, it's been way too long since I posted an update.

Lately, I've been playing Star Wars Commander on my phone. It's not a bad game, not terribly deep, but enjoyable nonetheless. When I opened it up today, I got a nice little popup: "Star Wars Commander is celebrating its one-year anniversary! Here are some free crystals. Touch this to learn more!" Great! I like free stuff, so let's look.

It links to this page which has a really cool infographic of all of the things that have been created in the game: 7 billion infantry, 40 million AT-ATs, etc. It's pretty neat.

Except for this.

Rebel spacecraft infographic

Um, what?

HOW DO THE STAR WARS PEOPLE NOT KNOW WHICH ONE IS AN X-WING AND WHICH ONE IS A Y-WING? THEY HAVE SHAPES!

xwing-ywing2

To be honest, this isn't making me feel all warm and fuzzy about the upcoming theme parks.

Update: I just noticed that the file name of the original infographic is SWC-infogaphics_Final-update.jpg. Note especially that it's called "Final-update". I can just imagine a designer sitting there, getting pissed off, about all of the changes to make it "accurate." Nice.

22May/080

Breakout in Model-View-Presenter

Posted by Rob

I sat down tonight and in about three hours or so cranked out the first vestiges of something that kind of maybe resembles Breakout, only using graphics that are possibly worse than anything ever shown on Atari 2600 because well, I made them.

Thanks to Joel Neubeck for the video player, and thanks to digital blasphemy for the background (yes I know it's not a free image, I'll be sure to remove it before it goes anywhere).

I should also point out: I used some sounds from the Spacewar demo in XNA Game Studio 2.0.  Those sound AWESOME.

This app is built with a model in a separate assembly; the model portion of the project contains the mathematics, collision detection, and all the game objects on the screen.  We've got tiles, a ball, and a paddle.  I should also point out that there's a game board (that's the big black empty space on the screen).

The XNA game application, then, instantiates a GameBoard (that's the containing class that owns all of the rest of the objects) and then instantiates a GameBoardPresenter.  GameBoardPresenter is part of the game application itself, and it owns a copy of the GameBoard because it's intimately tied to its inner workings.

To keep things relatively efficient, the GameBoard class exposes all of its object lists as properties that return the lists.  This way, object references aren't duplicated (except for the references to the Lists), and we're not wasting unnecessary memory and copy operations.  (If you're not clear what I mean, here are the properties:)

   1: public Paddle Player
   2: {
   3:     get { return m_player; }
   4: }
   5:  
   6: public List<Ball> Balls
   7: {
   8:     get { return m_balls; }
   9: }
  10:  
  11: public List<Tile> Tiles
  12: {
  13:     get { return m_tiles; }
  14: }
  15:  
  16: public List<Projectile> Projectiles
  17: {
  18:     get { return m_projectiles; }
  19: }

Since the balls are the only pieces of the game that move, each call to Update() on the GameBoard instance iterates through each Ball object, moving it accordingly and handling any collisions.  When a collision is detected, it fires an event corresponding to the type of collision, which is consumed by the GameBoardPresenter.

One other advantage of separating out the presentation and the actual game logic is that I can scale the graphics size to anything I want; for instance, the video above was recorded and 960x720, but I was also able to run it at 1920x1200 without any apparent differences in visualization.  The internal game units actually default to a game board size of 4800x4800, and scaling is simply done within the GameBoardPresenter's Draw() method:

   1: public void Draw(SpriteBatch target)
   2: {
   3:     Texture2D currentTexture;
   4:     Vector2 scale = new Vector2(m_scale);
   5:     Vector2 offset = new Vector2(m_xOffset, 0);
   6:  
   7:     currentTexture = m_textures[BoardItemType.GameBoard];
   8:     target.Draw(currentTexture, offset,  null, Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 1f);
   9:  
  10:     currentTexture = m_textures[BoardItemType.BasicTile];
  11:     foreach (Tile t in m_tiles)
  12:     {
  13:         target.Draw(currentTexture, t.Position * scale + offset, null, Color.White, 0f, Vector2.Zero, m_scale, SpriteEffects.None, 0f);
  14:     }
  15:  
  16:     currentTexture = m_textures[BoardItemType.Paddle];
  17:     target.Draw(currentTexture, m_paddle.Position * scale + offset, null, Color.White, 0f, Vector2.Zero, m_scale, SpriteEffects.None, 0f);
  18:  
  19:     currentTexture = m_textures[BoardItemType.Ball];
  20:     foreach (Ball b in m_balls)
  21:     {
  22:         target.Draw(currentTexture, b.Position * scale + offset, null, Color.White, 0f, Vector2.Zero, m_scale, SpriteEffects.None, 1f);
  23:     }
  24: }

The scale is calculated when the backbuffer is sized.

Source code for this isn't quite ready yet; there are some things that I want to refactor (for instance, I want the Model part to define input "actions" and that View/Presenter part to keymap inputs to actions). 

Tagged as: , , , No Comments
15May/080

Adding Collision Detection

Posted by Rob

There's already rudimentary collision detection in the app, but only for the sides.  What if I wanted the balls to detect when they collide?

In the demo video you can already see the balls colliding (and, incidentally, the gravity changing from down, then left, up, right, and down again).  But how did I get there?  Wouldn't you know that XNA has a built-in structure called BoundingSphere that can handle that for you?  I defined the bounding sphere as centered around the ball's center point (which was defined by the texture's dimensions), and used the z-coordinate as 0.  Intrinsically, then, the bounding sphere acts like a bounding circle as long as everything is on the same Z-plane.

Next, I created a couple interfaces that are going to help me out.  Interfaces such as IPhysical and IPhysicalSphere -- they provide necessary information for collidable objects.  Here they are:

   1: public interface IPhysical
   2: {
   3:     Vector3 CenterOfMass
   4:     {
   5:         get;
   6:     }
   7:  
   8:     Vector3 Speed
   9:     {
  10:         get;
  11:         set;
  12:     }
  13:  
  14:     float Mass
  15:     {
  16:         get;
  17:     }
  18: }
  19:  
  20: public interface IPhysicalSphere : IPhysical
  21: {
  22:     BoundingSphere Bounds
  23:     {
  24:         get;
  25:     }
  26: }

By implementing these properties, a separate class can be constructed to perform the necessary math for bouncing balls.  When first implemented, colliding balls would simply switch speed vectors.  However, that only works if balls collide straight on (so that movement vectors would be inverse of each other).  When balls collide like this:

Amazing MS-Paint Art!

(Awesome MS-Paint artwork, huh?)

When balls collide like that, they don't simply exchange vectors.  What actually happens is that the speed vectors are changed according to the normal line between the two circles (or the normal plane between the two spheres).

I created a static class to do this work for me:

   1: public static class Collision
   2: {
   3:     public static void ApplyCollision(IPhysicalSphere sphereA, IPhysicalSphere sphereB)
   4:     {
   5:         Vector3 x = sphereB.CenterOfMass - sphereA.CenterOfMass;
   6:         x.Normalize();
   7:  
   8:         Vector3 v1 = sphereA.Speed;
   9:         float x1 = Vector3.Dot(x, v1);
  10:  
  11:         Vector3 v1x = x * x1;
  12:         Vector3 v1y = v1 - v1x;
  13:  
  14:         float m1 = sphereA.Mass;
  15:  
  16:         x = -x;
  17:         Vector3 v2 = sphereB.Speed;
  18:         float x2 = Vector3.Dot(x, v2);
  19:  
  20:         Vector3 v2x = x * x2;
  21:         Vector3 v2y = v2 - v2x;
  22:  
  23:         float m2 = sphereB.Mass;
  24:  
  25:         float combinedMass = m1 + m2;
  26:  
  27:         Vector3 newVelA = (v1x * ((m1 - m2) / combinedMass)) + (v2x * ((2f * m2) / combinedMass)) + v1y;
  28:         Vector3 newVelB = (v1x * ((2f * m1) / combinedMass)) + (v2x * ((m2 - m1) / combinedMass)) + v2y;
  29:  
  30:         sphereA.Speed = newVelA;
  31:         sphereB.Speed = newVelB;
  32:     }
  33: }

This was inspired by a blog post I found that covered it exceptionally.  I wish I could name the offer but I couldn't find the author's name!

Calling this method to adjust the speed vectors is done whenever collisions are detected.  That part is handled in the Game class during the Update method:

   1: for (int i = 0; i < m_balls.Count; i++)
   2: {
   3:     Ball b = m_balls[i];
   4:     for (int j = i + 1; j < m_balls.Count; j++)
   5:     {
   6:         Ball test = m_balls[j];
   7:         if (b.Bounds.Intersects(test.Bounds))
   8:         {
   9:             Collision.ApplyCollision(b, test);
  10:             b.Update(gameTime, GraphicsDevice.Viewport);
  11:             test.Update(gameTime, GraphicsDevice.Viewport);
  12:             b = null;
  13:             break;
  14:         }
  15:     }
  16:  
  17:     if (b != null)
  18:         b.Update(gameTime, GraphicsDevice.Viewport);
  19: }

And that's about all there is to it!  New source code is up!  Here's the next demo:

One other thing -- as a note, the red ball with the orange gradient has a mass of 8.0f; the others have a mass of 4.0f.

15May/080

My First XNA Application: The Bouncing Ball

Posted by Rob

My first application in XNA!  It's a... well, it's a ball that bounces.

It's bouncing!!

I created a new Windows XNA 2.0 application project.  I decided to abstract away a Ball object, as well as a GravitySource object.  There's a lot of cross-talk -- I'm not sure if this is good or not -- but it's happy enough for me. :-)

Gravity seems to be pretty straightforward to implement; the applied speed is going to be a vector direction with a constant speed as the magnitude.  Here's the code for the gravity source class:

   1: public class GravitySource
   2: {
   3:     private const float GRAVITY = 9.8f;
   4:  
   5:     private Vector2 m_gravitySpeed;
   6:  
   7:     public void ApplyGravity(ref Vector2 currentSpeed)
   8:     {
   9:         currentSpeed += m_gravitySpeed;
  10:     }
  11:  
  12:     public void ResetToDirection(Direction target)
  13:     {
  14:         switch (target)
  15:         {
  16:             case Direction.Up:
  17:                 m_gravitySpeed = new Vector2(0f, -GRAVITY);
  18:                 break;
  19:             case Direction.Left:
  20:                 m_gravitySpeed = new Vector2(-GRAVITY, 0f);
  21:                 break;
  22:             case Direction.Right:
  23:                 m_gravitySpeed = new Vector2(GRAVITY, 0f);
  24:                 break;
  25:             case Direction.Down:
  26:                 m_gravitySpeed = new Vector2(0f, GRAVITY);
  27:                 break;
  28:             case Direction.DownLeft:
  29:                 m_gravitySpeed = Vector2.Normalize(new Vector2(-1, 1)) * GRAVITY;
  30:                 break;
  31:             case Direction.DownRight:
  32:                 m_gravitySpeed = Vector2.Normalize(new Vector2(1, 1)) * GRAVITY;
  33:                 break;
  34:             case Direction.UpLeft:
  35:                 m_gravitySpeed = Vector2.Normalize(new Vector2(-1, -1)) * GRAVITY;
  36:                 break;
  37:             case Direction.UpRight:
  38:                 m_gravitySpeed = Vector2.Normalize(new Vector2(1, -1)) * GRAVITY;
  39:                 break;
  40:         }
  41:     }
  42: }

Note that I added a Direction enumeration to make it nice to read.

The game class has a reference to the ball object, and it handles the keyboard input to change the gravity direction all within the Update method:

   1: protected override void Update(GameTime gameTime)
   2: {
   3:     // Allows the game to exit
   4:     if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
   5:         this.Exit();
   6:  
   7:     KeyboardState keybstate = Keyboard.GetState();
   8:     if (keybstate.IsKeyDown(Keys.Down))
   9:     {
  10:         if (keybstate.IsKeyDown(Keys.Left))
  11:         {
  12:             m_grav.ResetToDirection(Direction.DownLeft);
  13:         }
  14:         else if (keybstate.IsKeyDown(Keys.Right))
  15:         {
  16:             m_grav.ResetToDirection(Direction.DownRight);
  17:         }
  18:         else
  19:         {
  20:             m_grav.ResetToDirection(Direction.Down);
  21:         }
  22:     }
  23:     else if (keybstate.IsKeyDown(Keys.Up))
  24:     {
  25:         if (keybstate.IsKeyDown(Keys.Left))
  26:         {
  27:             m_grav.ResetToDirection(Direction.UpLeft);
  28:         }
  29:         else if (keybstate.IsKeyDown(Keys.Right))
  30:         {
  31:             m_grav.ResetToDirection(Direction.UpRight);
  32:         }
  33:         else
  34:         {
  35:             m_grav.ResetToDirection(Direction.Up);
  36:         }
  37:     }
  38:     else if (keybstate.IsKeyDown(Keys.Left))
  39:     {
  40:         m_grav.ResetToDirection(Direction.Left);
  41:     }
  42:     else if (keybstate.IsKeyDown(Keys.Right))
  43:     {
  44:         m_grav.ResetToDirection(Direction.Right);
  45:     }
  46:  
  47:     m_ball.Update(gameTime, graphics.GraphicsDevice.Viewport);
  48:  
  49:     base.Update(gameTime);
  50: }

The game's drawing method is actually quite simple:

   1: protected override void Draw(GameTime gameTime)
   2: {
   3:     graphics.GraphicsDevice.Clear(Color.Black);
   4:  
   5:     spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
   6:     m_ball.Render(gameTime, spriteBatch);
   7:     spriteBatch.End();
   8:  
   9:     base.Draw(gameTime);
  10: }

And that gets implemented in the Ball class; here are the meat and potatoes:

   1: public void Render(GameTime time, SpriteBatch spriteBatch)
   2: {
   3:     spriteBatch.Draw(m_texture, m_position, Color.White);
   4: }
   5:  
   6: public void Update(GameTime time, Viewport bounds)
   7: {
   8:     m_position += m_speed * (float)time.ElapsedGameTime.TotalSeconds;
   9:  
  10:     int maxX = bounds.Width - m_texture.Width;
  11:     int maxY = bounds.Height - m_texture.Height;
  12:  
  13:     if (m_position.X > maxX)
  14:     {
  15:         m_speed.X *= -1.0f;
  16:         m_position.X = maxX;
  17:     }
  18:     else if (m_position.X < 0)
  19:     {
  20:         m_speed.X *= -1.0f;
  21:         m_position.X = 0;
  22:     }
  23:     else
  24:     {
  25:         // else we're in a freefall to the right!
  26:         m_gravity.ApplyGravity(ref m_speed);
  27:     }
  28:  
  29:     if (m_position.Y > maxY)
  30:     {
  31:         m_speed.Y *= -1.0f;
  32:         m_position.Y = maxY;
  33:     }
  34:     else if (m_position.Y < 0)
  35:     {
  36:         m_speed.Y *= -1.0f;
  37:         m_position.Y = 0;
  38:     }
  39:     else
  40:     {
  41:         // else we're in a freefall
  42:         // v(t) = at + k
  43:         // in this case a = -9.8
  44:         // Inverted because we're going down which is positive Y ;-)
  45:         m_gravity.ApplyGravity(ref m_speed);
  46:     }
  47: }

One kind of odd part is that it's designed to be simply a perfectly elastic ball (m_speed.Y *= -1.0f) but it actually bounces higher over time.

Source code is downloadable here.  Thanks to Betsy Aoki's samples online - they were incredibly helpful!

Tagged as: , , No Comments
5Feb/080

Mass Effect: Promised the Galaxy, Delivered… a Star System

Posted by Rob

Oooh, shiny!Mass Effect, the latest installment of Bioware's epic RPGs, promised us the world, nay, the galaxy.  It spoke of a universe of incredible depth, a future world in which humanity has discovered the means by which to travel to distant stars, interact with aliens, and eek our our place in the galaxy.  It's got an incredibly compelling story, in which you are selected as humanity's first "Spectre," an agent of the galactic government empowered to do anything you see fit to preserve galactic stability.  The visual artwork is compelling; the game has a "graininess filter," to make it look like an old 80's sci fi movie, and the music fits right into that genre (it makes the game feel all nostalgic).  The use of HDR is gorgeous, and while at times it might feel a little overused, in general it is fantastic.

Graphics

Although graphical quality is excellent, there is one exception: load times.  Please, take five more seconds to load the textures and the bump maps.

Load Times - a mass immersion breaker

As you can see in the photo to the left (I apologize, they were taken from my digital camera pointed at my TV), the bump map and texture map on the ground didn't load immediately.  Take a few extra seconds and load the bump maps before I get into the game.  In my opinion, it's incredibly immersion breaking, and should have been unacceptable to Bioware.  Unless load time was one of the highest-priority requirements, which I can't imagine (and I'll say why in a second), it amazes me that it made it to production.

One of the COOL things about loading in this game, though, is the elevator load style.  Loading happens in a lot of areas by going into elevators - definitely one of the least immersion-breaking aspects of the game.  That was a fantastic move, and in fact while we're on the Citadel, you can pick up information on new missions and get a little more information about your squad members.  It was definitely one of the strongest parts of the game.

Gameplay

Where did it go?

This is without a doubt one of the best games I've ever played in terms of control.  It is incredibly easy to control the characters, the selection of dialogue is fantastic, and the menu interface is great (with the single exception of - surprise, surprise - inventory management).

One thing I'm curious about - the E3 2006 dialog depicted to the left - where did it go?  "A billion lives are hanging in the balance here.  I won't let some piss-ant bartender slow me down."  That seemed like such a great scene.  I'm disappointed that it's not in the game.

There is one downer - the vehicle is ridiculous in terms of control.  I'm on my third playthrough, and I haven't figure out how to control it steadily.  One thing I totally loved, though, was the running-over the Geth.  It was fun to run them into the lava on the planet, incidentally, shown in the image with the bad load times.

I also don't like the mission assignments.  Besides the main quests, it's annoying to hear from the Systems Alliance admiral whom you never meet, who just says "You're a Spectre, and you answer to the Council, but you're still a human."  Come on, give me a break.  Seth Green, give me a chance to say no - don't just say "Transmission comin' in - patchin' it through" every freaking time I look at the Galaxy Map.  PLEASE, SETH GREEN, LET ME DO MY OWN THING!

Music

The music is absolutely fantastic.  It's one of the best soundtracks I've ever heard; it's absolutely distinctive.  And it was inexpensive to buy on iTunes!  Other than that, I can't say much.

Finally - the Universe

I think this is possibly Mass Effect's greatest weakness, as well as the greatest strength.  While places like the Citadel are built up very well, the Citadel feels like about 60% of the universe's civilization.  That's pretty sad considering that they indicate a few million (maybe seven million) people live on the Citadel, and there have to be hundreds of billions, if not trillions, of people across the galaxy.  The planets you can visit outside of the main quest feel forced and random, not to mention barren.  It's supposed to be the galaxy - why do I hardly ever run into people who aren't trying to kill me?

But on the other hand, the way the other races are introduced and described is incredibly thorough.  It's exactly how I'd like to see a game introduce the races it has.  I can't sing the praises of the story or the universe (in this sense) enough.

Go buy it, if you haven't yet.  It's worth the cash.  Get it new - support BioWare.

Tagged as: , , No Comments