Taking a break for a week or so, going to fly down south, hang out with my in-laws. Speaking of airports & in-laws, here's a good book to read when you're bored:
"The Design of Everyday Things" is probably the best non-programming book a programmer can read.
As programmers it's our job is to construct systems that make life easier (or just more fun) for someone else. When a user launches our program for the first time, they do so without any knowledge of how it works, or any knowledge of how we expected them to use it. "The Design of Everyday Things" focuses on that moment of initial user interaction, exploring the reasons for a users "success" or "failure" with systems, both foreign and familiar. It mainly boils down to the user interface, and that's what this book is about - building user interfaces that make sense.
Using familiar things like telephones and refrigerators, the author demonstrates how common things, even simple things we have a lot of experience with, can be rendered confusing by an interface that does not convey proper usage to the user. Ever been stopped short by pushing a door that must be pulled? Sometimes stupid mistakes are just stupid mistakes, but sometimes they're actually encouraged by devices that don't naturally convey their proper use.
It's an interesting book that contains lots of useful concepts for anyone who designs anything for other people to use.
Tonight, my goal was to simply make sure I could occlude tiles correctly using the technique illustrated in the 2D camera article.
A 200 x 200 tile grid of tiles is defined, each tile is 64x64 pixels. A 320x240 camera is affixed to the player's ship (you can see it's white outline in the video), and as the ship moves around, the camera position is transformed, a bounding box is created around the transformed camera, this bounding box is "snapped" to the grid, and a subset of the grid is readily obtained for drawing. I ended up using even less / more simplistic math than I had assumed it would require, you don't really have to use mod arithmetic to do this, you can just mix & match Math.Clamp, .Floor & .Ceiling and pretty much get your array indexes straight away (there will be a fully verbose tutorial on all this when it's done, skip the details for now).
Now that the occlusion works, hopefully I'll get a chance to finish this tomorrow by adding the necessary transforms to make it behave like a camera (ship stays in the center of the screen, game world scrolls, that sort of thing). I think I'll need to use a viewport object to physically clip the excess, although I'm not sure yet. Lots of fun stuff to learn still.
The VB.NET XNA Project Template for Visual Studio has been updated to include the latest version of the VBContentManager.
If you have previously placed the VB.NET XNA Project Template into your Visual Studio templates folder, after installing Game Studio 2.0 you should overwrite the older template .zip file with the newer version now available.
Game Studio 2.0 beta has ended and Game Studio 2.0 has been officially released. There are breaking changes in the ContentPipeline.targets file, so I've updated the VBContentManager class to generate a Content.contentproj file that works within the new parameters. The usage of the class remains the same.
Works with Game Studio 2.0
Now outputs content to the appropriate Visual Studio bin location (i.e., bin\Debug or bin\Release, whichever you've set Visual Studio to)
Public Fonts As Hashtable(Of SpriteFont) Public Textures As Hashtable(Of Texture2D) Public SubTextures As Hashtable(Of Hashtable(Of Rectangle)) Public Sounds As Hashtable(Of SoundBank) Public Effects As Hashtable(Of Effect)
Private strContentprojFile, strProjectFolder, strExecutingFolder, strContentFolder As String Private objContent As List(Of ContentFile) Private intDupeSound, intDupeTexture, intDupeFont, intDupeEffect AsInteger
Public ContentType As ContentTypes Public File As System.IO.FileInfo Public Name As String
PublicSubNew(ByVal ContentType As ContentTypes, ByVal ContentFile As System.IO.FileInfo, ByVal Name As String) Me.ContentType = ContentType Me.File = ContentFile Me.Name = Name EndSub
EndStructure
''' <summary> ''' Creates a new ContentManager which has been extended with functionality to support a Visual Basic (or other non-C#) project ''' </summary> ''' <param name="ServiceProvider">Your game has a .Services property; send that in here.</param> ''' <param name="ContentFolder">Your content folder (a relative path, from your game's Project node as it appears in the Solution Explorer)</param> ''' <remarks></remarks> PublicSubNew(ByVal ServiceProvider As IServiceProvider, ByVal ContentFolder As String)
MyBase.New(ServiceProvider)
With System.Reflection.Assembly.GetEntryAssembly.Location Me.strExecutingFolder = .Substring(0, .LastIndexOf("\")) EndWith
Me.Fonts = New Hashtable(Of SpriteFont) Me.Textures = New Hashtable(Of Texture2D) Me.SubTextures = New Hashtable(Of Hashtable(Of Rectangle)) Me.Sounds = New Hashtable(Of SoundBank) Me.Effects = New Hashtable(Of Effect)
EndSub
''' <summary> ''' Attempts to load all assets which have been compiled by the Content Pipeline. Assets are placed into the respective Hash table (Fonts, Textures, et al) ''' </summary> ''' <remarks></remarks> PublicSub LoadAllContent()
Dim objCompiledContent AsNew List(Of IO.FileInfo) Dim strUnknownFiles As String = ""
ForEach file As IO.FileInfo InNew IO.DirectoryInfo(Me.strExecutingFolder).GetFiles("*.xnb", IO.SearchOption.AllDirectories)
'Is this a sheet w/ a rectangle map? If so, load the rectangle map
If IO.File.Exists(file.FullName.Replace(file.Extension, ".map")) Then Me.LoadMap(file.Name.Replace(file.Extension, ""), file.FullName.Replace(file.Extension, ".map")) EndIf
MsgBox("The VBContentManager was unable to determine the correct loader for:" & vbCrLf & _ strUnknownFiles & vbCrLf & vbCrLf & _ "Unless loaded by something else, the above content won't be available at runtime." & vbCrLf & vbCrLf & _ "This application may become unstable as a result.")
EndIf
EndSub
PrivateSub LoadMap(ByVal strTextureNameName As String, ByVal strMapFile As String)
Dim objReader As IO.StreamReader Dim strMapData As String Dim strLine() As String Dim strItem() As String
objReader = New IO.StreamReader(strMapFile)
Me.SubTextures.Add(strTextureNameName, New Hashtable(Of Rectangle))
Me.SubTextures(strTextureNameName).Add(strItem(0), New Rectangle(CInt(strItem(1)), CInt(strItem(2)), CInt(strItem(3)), CInt(strItem(4))))
Next
EndSub
''' <summary> ''' Compiles the VB Content Pipeline ''' </summary> ''' <param name="stealth">Hide/Show the MSBuild window during the compile process</param> ''' <param name="platform">If there's a way the Xbox360 will work with VB, you could pass in the Xbox parameter as a target platform to MSBuild. But it's Windows by default.</param> ''' <remarks></remarks> PublicSub CompileContent(ByVal stealth AsBoolean, OptionalByVal platform As Platform = VBContentManager.Platform.Windows)
Dim objProcess As Process Dim objPSI AsNew Diagnostics.ProcessStartInfo
Me.objContent = New List(Of ContentFile)
Me.HarvestContent()
IfMe.objContent.Count > 0 Then
Me.BuildContentProj(platform)
Try 'to write the content project file to disk
Using TempStreamWriter As System.IO.StreamWriter = New System.IO.StreamWriter(Me.strProjectFolder & "\Content.contentproj", False)
MsgBox("When building the pipeline, trying to run MSBuild.exe resulted in: " & ex.Message)
EndTry
Catch ex As Exception
MsgBox("When building the pipeline, trying to create the Content.contentproj file """ & Me.strProjectFolder & "\Content.contentproj"" resulted in: " & ex.Message)
EndTry
EndIf
EndSub
PrivateSub HarvestContent()
Dim objRoot As System.IO.DirectoryInfo = New System.IO.DirectoryInfo(Me.strContentFolder)
ForEach sound As System.IO.FileInfo In objRoot.GetFiles("*.xab", IO.SearchOption.AllDirectories) IfMe.objContent.Contains(New ContentFile(ContentTypes.Sounds, sound, sound.Name.Replace(sound.Extension, ""))) Then Me.intDupeSound += 1 Me.objContent.Add(New ContentFile(ContentTypes.Sounds, sound, sound.Name.Replace(sound.Extension, "") & Me.intDupeSound.ToString)) Else Me.objContent.Add(New ContentFile(ContentTypes.Sounds, sound, sound.Name.Replace(sound.Extension, ""))) EndIf Next
ForEach texture As System.IO.FileInfo In objRoot.GetFiles("*.png", IO.SearchOption.AllDirectories) IfMe.objContent.Contains(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, ""))) Then Me.intDupeTexture += 1 Me.objContent.Add(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, "") & Me.intDupeTexture.ToString)) Else Me.objContent.Add(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, ""))) EndIf Next
ForEach texture As System.IO.FileInfo In objRoot.GetFiles("*.jpg", IO.SearchOption.AllDirectories) IfMe.objContent.Contains(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, ""))) Then Me.intDupeTexture += 1 Me.objContent.Add(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, "") & Me.intDupeTexture.ToString)) Else Me.objContent.Add(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, ""))) EndIf Next
ForEach texture As System.IO.FileInfo In objRoot.GetFiles("*.gif", IO.SearchOption.AllDirectories) IfMe.objContent.Contains(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, ""))) Then Me.intDupeTexture += 1 Me.objContent.Add(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, "") & Me.intDupeTexture.ToString)) Else Me.objContent.Add(New ContentFile(ContentTypes.Textures, texture, texture.Name.Replace(texture.Extension, ""))) EndIf Next
ForEach font As System.IO.FileInfo In objRoot.GetFiles("*.spritefont", IO.SearchOption.AllDirectories) IfMe.objContent.Contains(New ContentFile(ContentTypes.Fonts, font, font.Name.Replace(font.Extension, ""))) Then Me.intDupeFont += 1 Me.objContent.Add(New ContentFile(ContentTypes.Fonts, font, font.Name.Replace(font.Extension, "") & Me.intDupeFont.ToString)) Else Me.objContent.Add(New ContentFile(ContentTypes.Fonts, font, font.Name.Replace(font.Extension, ""))) EndIf Next
ForEach effect As System.IO.FileInfo In objRoot.GetFiles("*.fx", IO.SearchOption.AllDirectories) IfMe.objContent.Contains(New ContentFile(ContentTypes.Effects, effect, effect.Name.Replace(effect.Extension, ""))) Then Me.intDupeEffect += 1 Me.objContent.Add(New ContentFile(ContentTypes.Effects, effect, effect.Name.Replace(effect.Extension, "") & Me.intDupeEffect.ToString)) Else Me.objContent.Add(New ContentFile(ContentTypes.Effects, effect, effect.Name.Replace(effect.Extension, ""))) EndIf Next
EndSub
PrivateSub BuildContentProj(ByVal platform As Platform)
If content.ContentType = ContentTypes.Textures Then
'If this texture is a sheet w/ a map, copy it's map out in the executable area 'along side the existing or eventual compiled texture
If IO.File.Exists(content.File.FullName.Replace(content.File.Extension, ".map")) Then
Try IO.File.Copy(content.File.FullName.Replace(content.File.Extension, ".map"), _ Me.strExecutingFolder & content.File.FullName.Replace(Me.strProjectFolder, "").Replace(content.File.Extension, ".map"), True) Catch ex As Exception 'I'm going to be optimistic and assume that if I can't copy 'it's because the file is already there, and life is just fine EndTry
EndIf
EndIf
Next
Me.strContentprojFile &= vbCrLf & vbTab & "<!-- To modify your build process, add your task inside one of the targets below and uncomment it. " & vbCrLf & _ vbTab & " Other similar extension points exist, see Microsoft.Common.targets." & vbCrLf & _ vbTab & "<Target Name=""BeforeBuild"">" & vbCrLf & _ vbTab & "</Target>" & vbCrLf & _ vbTab & "<Target Name=""AfterBuild"">" & vbCrLf & _ vbTab & "</Target>" & vbCrLf & _ vbTab & "-->" & vbCrLf & _ "</Project>"
Create a "bounding box" (orange) around the camera (blue), snap that bounding box to the grid on all sides (moving outwards, in all directions from the bounding box, until you hit a grid line - green). Then just divide the corners of the green square by tilesize, you should end up with actual grid array element positions to fetch tiles with.
There's a little slop when the camera is rotated (you'll pick up extra tiles you don't really need to draw & update) but, on the other hand, you don't have to scan around or perform any intersect tests, so hopefully the net result is reasonably quick.
If this doesn't work I'm screwed, because I can't think of any other way to do it! Now I just have to write it...
Update: I got about half way through it this evening and figured I'd run it just to see what happens. Here's some "unexpected results" for you:
One of the things I want to feature in Star Maze are ship upgrades, allowing you to attach different thrusters & weapons to your ship.
Given the parent/child system, we know that we can "attach" one sprite to another, like what we did in Tutorial 7. And since our game objects are nothing more than "sprites with a brain", we can actually attach one game object to another. So after incorporating the psuedo-physics parameters & processing to the sprite class, I created a game component meant soley to be a child of another; a thruster module which can be attached to anything to make it move. This is the base thruster class from which you derive to create a custom thruster object:
PublicInterface IThruster Sub StopThrusterAnim() Sub StartThrusterAnim() EndInterface
PublicSubNew(ByVal parent As Ship, ByVal type As ThrusterType, ByVal key As Keys, ByVal name As String, ByVal force AsSingle, ByVal containerSize As Vector2) MyBase.New(Program.StarMaze, parent, name, True, True, containerSize) Me.sngForce = force Me.enumKey = key Me.enumType = type EndSub
PublicSubNew(ByVal parent As Ship, ByVal type As ThrusterType, ByVal key As Keys, ByVal name As String, ByVal force AsSingle, ByVal sheet As String, ByVal subtexture As String) MyBase.New(Program.StarMaze, parent, name, True, True, sheet, subtexture) Me.sngForce = force Me.enumKey = key Me.enumType = type EndSub
EndClass
With the base thruster class you can create a rotational thruster for spinning around or a linear thruster for vector movement. A keyboard key parameter will be monitored by the thruster to turn itself on and off, and there's a parameter to hold how much force the thruster applies to it's parent (whatever it's attached to).
When it Updates, it calls the ApplyForce or ApplyRotationalForce methods of it's parent, and the system moves accordingly.
A simple interface defines two routines, StopThrusterAnim & StartThrusterAnim, which provides the implementor with a place & time to perform graphical feedback as it relates to the thruster entering an On or Off state.
Using this base thruster class I created 4 different game components; custom thrusters which I can attach to the player's ship. Two different aft thrusters (linear), a port thruster (clockwise rotational), and a starboard thruster (counterclockwise rotational).
The player's ship object can now be defined very simply, by creating instances of these thrusters and attaching whichever ones we want accordingly:
PublicClass Ship Inherits Sprite
Protected aftThruster, portThruster, starboardThruster As BaseThruster
In the video I start out using one of two aft thruster classes which derive from base thruster (the green one, it produces 500 pixels per second (pps) of linear force). When the app stops and I go into the IDE, I swap out the green 500pps thruster for a different thruster class, a red one that produces 250 pps of linear force. You my also be able to see the tiny little port and starboard thrusters applying the counter- and clockwise rotational forces as well.
As you can see from the Ship class code above, the ship itself has no inherit way to move itself; it's just a hull that doesn't handle keyboard input or anything of the sort. To make it move, we simply attach thruster components. The neat part about this system is that you can attach any thruster you want to any game object you want. You can put thrusters on the player's ship, on enemy AI ships, you can stick one on a bowling ball if you want. The thruster component is a somewhat generic way to apply force, to any game object.
My laptop was lagging for unrelated reasons when I recorded this, so the fps is kinda low, but in an otherwise normal environment the code runs quite nicely.
So in developing a new system of sprite movement, I decided to try a little bit of really basic physics. Adding a few simple members to the sprite class to store values relating to velocity, momentum & friction:
Protected objVelocity As Vector2 Protected sngFriction, sngMass AsSingle
and then, adding method called ApplyForce:
PublicSub ApplyForce(ByVal force As Vector2)
IfMe.sngMass > 0 Then Me.objVelocity += (force / Me.sngMass) Else Me.objVelocity += force EndIf
EndSub
and finally, processing this information during update:
PublicSub UpdatePhysics()
Dim time AsSingle Dim negX, negY AsBoolean
'Our unit of measure is pixles per second time = Me.Game.GameTime.ElapsedGameTime.TotalMilliseconds / 1000
'If there is any velocity...
IfMe.objVelocity <> Vector2.Zero Then
'before we apply friction, let's remember whether 'or not we started out with positive or negative 'velocity value, for both x and y, by setting '"wasNegativeX/Y" booleans
'simulate some psuedo-friction by normalizing the 'velocity vector and multiplying it by the friction 'value. This gives us an amount to "push" the object 'in the exact opposite direction. Multiplied by 'time to do just enough for this frame only. Subtract 'the result from the object's velocity, to slow it down.
Me.objVelocity -= (Vector2.Normalize(Me.objVelocity) * Me.sngFriction) * time
'Because friction is simulated by pushing the object in 'the opposite direction, towards the end when the object 'is almost stopped we don't want the psuedo-friction to 'start pushing the object backwards; at most, friction 'stops something, but it shouldn't ever move it. So using 'or cached "whether or not it was negative" booleans, 'check to see if the polarity of our X/Y velocity changed 'due to friction. If it did, snap the velocity of that 'value to 0.
If (negX AndMe.objVelocity.X > 0) Or (Not negX AndMe.objVelocity.X < 0) Then Me.objVelocity.X = 0 EndIf
If (negY AndMe.objVelocity.Y > 0) Or (Not negY AndMe.objVelocity.Y < 0) Then Me.objVelocity.Y = 0 EndIf
'increment the object's position by the amount of 'velocity appropriate for this frame by multiplying 'by the time value.
Me.Position += (Me.objVelocity * time)
EndIf
EndSub
I haven't really discussed my existing system for movement on the blog yet; we'll get to that eventually, but I was amused by this velocity/friction/mass approach and it's probably more relevant to the needs of gaming anyway.
This is far from an actual simulator, I'm only using mass to resist initial force and not really applying it to friction but that's ok. This is supposed to be simple, and the results aren't too shabby.
In the video, just the new "physics" values introduced are being used to move the spaceship around. While the player holds down the thrust key, the current orientation of the ship * some thrust value determines a parameter for ApplyForce. Using a PlayerShip game object (as described in previous tutorials, it just inherits from sprite), I created a ThrusterOn method. As long as the user holds the truster key down, I call PlayerShip.ThrusterOn
PublicSub ThrusterOn()
Dim time AsSingle
time = Me.Game.GameTime.ElapsedGameTime.TotalMilliseconds / 1000
The ship has a little mass to it, so it resists force somewhat (this gives you a way to tweak acceleration curve vs. thruster power), and the ship's psuedo-friction value constantly pulls it's velocity toward zero. This example was built using my "real" 2D engine (which is a little further along than what the blog has covered so far, i.e. there's keyboard handling & debugging & screen management, etc.), but the movement you see is achieved through the simple additions described above.
The first animation system I created, which I currently use, was designed with UI components in mind. Things like buttons, windows, forms & controls, stuff like that. The current animation system I have for this is very good at moving something from one predefined point to another: from A to B. Or from rotation value X to rotation value Y. But always from one finite value to another finite value.
I'll keep this around, because it's useful, but games are not so rigidly constructed as UIs. I'm forming the opinion that it's better to not rely on knowledge of predefined points when moving game objects, but rather rely on movement values such as velocity and direction, and just react accordingly to those values over time. So in consuming yet more R&D time before I put any more tutorials out, I'm going to construct such a thing. I have a few ideas I want to play with which may prove very useful.
I'm also working on a grid object which can be incorporated into the parent/child system so that at any point, a grid w/ a camera can be arbitrarily put anywhere & globally transformed by it's parent.
I've been surprised by how much GPU power it takes to run a pixel shader over a scene. The 2-pass Gaussian blur I've been testing with pushes my laptop's GPU pretty hard (a Mobility FireGL V5200; not exactly an uber GPU). Initially I attributed the performance I was seeing to my code being inefficient, but after comparing performance with the post processing samples on the Creator's Club site (which I presume to be well formed) I don't think I'm doing anything "wrong" - I'm just a little bit brainwashed by the performance of modern CPUs.
A modern CPU (like an Intel core duo 2.0Ghz processor) can perform mathematical operations so fast as to be indistinguishable from magic. But I was forgetting that the modern GPU hasn't had as much time as the CPU to mature & advance as a technology. GPUs are fast, but they still have plenty of room to grow. In thinking my code was inefficient, in truth I was overestimating the ability of my laptop's GPU.
So the shader business is basically done. I've got a system of renderTargets built into the engine to handle arbitrary scene processing at arbitrary points in the parent/child hierarchy, which accomplishes the goal. Now all I need to do is steal my wife's 8800GTX out of her gaming rig and cram it into my laptop with a vice. Yea, that should work.
My job has me somewhat diverted from this blog, although, the stuff I'm tasked with at work is vicariously related. In my current project, a "mostly complete" version of this 2D engine is used to create an XNA application. But I'm a "regular" programmer, not a "game developer", and so the goal of this 2D engine is to create a really user-friendly, neato touch-screen interface in an attempt to delude all who use it into believing their job is not quite as boring and mundane as it is. Which is not such a bad thing to do for people, really.
This app at work likes to get real chummy with a SQL database to conduct important workly business. About a 3rd of this chummy relationship is facilitated through the DataAdapter.Fill method, mainly because I have a small ADO engine with some built in concurrency checking features etc which I'm reusing in the XNA app. What's particularly problematic about using DataAdapter.Fill with an XNA application is the fact that DataAdapter.Fill is a synchronous method: this means DataAdapter.Fill "blocks the calling thread" while it's grabbing data out of SQL. I'm no game industry veteran by any means, but I can guess blocking the main thread of a game probably ranks in the top 3 "worst design features of all time". While it just so happens I can get away with this over the LAN, deploying this app over a VPN is obviously not going to work.
So currently I'm devising an async system for DataAdapter.Fill and just basically making all the changes I have to make to get this done, it's a little extensive because I'm revamping an entire remote access system, just a little. To be honest it's something I've always known I should do, I just never had a real critical reason to do it. Kind of a hassle but certainly for the best.
But this relates partly to my 2D engine / game programming hobby in that I'll end up with a model whereby the game loop submits requests for remote data, then continues to render normally while it polls for the result to complete. I would assume this sort of model will be handy for a networking layer to a multiplayer game, like for sending requests to a game server or peer client and awaiting the response. So, mundane as it is, this sort of thing should prove reasonably useful in the future.