Thursday, June 19, 2008

Day 159: Profiling

Today was my first in-depth profiling. Yesterday I did some looking over, but even with ruby-prof's excellent HTML output, it was taking me a while to get my head around exactly where the bottlenecks in my code were.

Then I found out that ruby-prof can output in a format that KCacheGrind understands. I also found out that KCacheGrind exists, which was likewise helpful, and suddenly I had a tool which could walk me through parts that needed optimizing.

My first surprise was that Set::merge was called FOUR MILLION TIMES. No, I'm not making that number up. When you ask a KuiObject for its attributes, children, etc, it collects all of the ones it knows about in a set and then merges it with the ones its superclass knows about. Except there was no caching here, it was doing it every single time.

Even that wouldn't have been a problem, because you'd think I wouldn't be asking what every single attribute of an object is except when I'm loading/saving (and then time is not as important). But it turns out that it was heavily used in comparison. Two KuiObjects are == if all their children, attributes, etc, are equal, and this requires me to call Set::merge above.

So I fixed the caching problem, and that sped things up. Then I made it so that two KuiObjects are == if their tags are ==. This changed a dozen+ string comparisons and countless object comparisons into one string comparison. That sped things up a lot.

The beauty of using profiling is that I can tell whether harebrained ideas I have for optimization actually work. For instance, I thought that the string comparisons were likely to also be too slow, and I figured symbol comparisons would be faster. Turns out, no, they're not, converting the strings to symbols and comparing them is a little bit slower than just comparing the strings to begin with.

My other optimization idea, to take a line of the form (angle / 10).to_i and change it to (angle.to_i / 10), did speed things up a bit, due to the fortunate elimination of floating point division.

At this point the game is smooth again in sectors with lots of ships, but not when they start shooting the hell out of someone. There are a few other spots in the code I can optimize, fortunately. I would like to avoid having to drop down to C, as I know that this more likely indicates a failure to optimize on my part than it does a shortcoming on Ruby's part.

Day 158: Reno

I'm currently attending the Smalltalk Solutions conference in Reno, where we sent a man the #kill message, just to see if he understood.

Trust me, if you know Smalltalk, that's a very funny joke.

Good news/bad news on the development front. The good news is that I finished the two tickets I mentioned: #179 was for making lasers that weren't slower than the ships that fired them, and #180 was all about adding "scenery" ships to the tutorial.

That's where I ran into the bad news. The first problem was that adding weapons to the ship in the editor still wasn't working, despite all the testing I'd done. What happened on the back end is that any time you give an object a tag, if it had a tag before it deletes it from the repository and re-registers with the new tag. Whenever I created a new weapon for a ship, it replaced the original! So if I had 2 lasers on board, suddenly the object in the repository would be those same two lasers. It was a mess to track down, but thankfully a relatively easy fix.

The worse news was that it's slow! There are noticeable hiccups in even sectors where there's no combat. The traditional profiling approach of using 'ruby -r profile' had so much overhead I couldn't actually run the game. Thankfully, Ruby-prof exists. It's got a small enough overhead that I can actually play my game.

I have a lot of data to go over for optimizing, and I'm not releasing the tutorial until I'm done, so I have one more ticket to close, #96, which I have aptly titled "OH GOD MY CPU".

For those interested in my contest entry, I am temporarily hosting it on my own machine, so get it while it's still up! I think the contest turned out really well and I'm happy that my entry ended up as good as it was.

Friday, June 13, 2008

Day 153: Contest

I'm taking today, tomorrow, and Sunday off to participate in the RubyWeekend game creation contest. This is the first extended break I've taken from the game since I moved into my new house, and I need it!

I did get some stuff done yesterday, mostly tweaks. Specifically, fixing the combat tutorial and making tutorial items cost money.

This puts me two tickets away from finishing the milestone and therefore the tutorial. I'll probably do a few things at that point:

  1. Create a github project for the game and upload it there.

  2. Branch the current version off and name it kuiper_rubygame_3.0.0. This will be a reference branch, in case I need to port everything back to Rubygame 3. Why make this branch? Because of the next step:

  3. Backport the game to Rubygame 2.3. I've had people interested in at least downloading and playing with the game, and as I'm using the bleeding edge my-version-may-break-daily version of Rubygame, it discourages people from doing so. I'll have the Rubygame 3 branch ready when I need to switch over. Finally, I'll:

  4. Release a 'preview'. This will consist of the tutorial and be announced here and maybe a few other places (forums only, no formal announcements yet). Get feedback, etc.


And after that, I start work on the main campaign!

Wednesday, June 11, 2008

Day 151: It's Never Easy

Today's subject is a ripoff of a line from the Phantasm series of movies. The movie's been around for as long as I've been alive, so I'm about to spoil the ending.

When the intrepid heroes finally kill the bad guy, they sit back and have a moment to recuperate. They do all their "Sure is better now that the bad guy's dead!" talk, and one of them says "Thank God, it's over"

That's when the bad guy appears behind them, stating "It's Never Over", and the movie ends. All the sequels followed this pattern. This was the first horror movie I saw that that didn't have a "protagonists escape and everything's better" ending (I hadn't yet seen A Nightmare on Elm Street)

So just when I think I've got an easy task ahead of me, I open the editor and the Tall Man appears, looming, intoning "It's Never Easy!" and then throwing killer spheres with knives into my head. Trust me, this is very scary if you've seen the movie. Especially if you saw it at the age of six.

The easy task that actually was easy today was #99, better radar coloration. Since everyone can get ahold of the player's ship, it was trivial to change the color if the current ship was either the target of the player or targeting the player.

I had plenty of time left over after that, though, so I tried my hand at #184, having a 'nearest hostile target' button. Writing it was not hard, and as it turns out the logic was mostly right. The problem came in testing. I'd start up the game, shoot someone until they were angry at me, and hit the button.

The game would freeze.

What this usually means is that it's about to crash and Ruby is about to give me a stacktrace. I don't know if it's Rubygame or my machine or what, but sometimes this takes anywhere from 5 seconds to several minutes to actually happen, and it's eating up 100% of the CPU the entire time. Very inconvenient for debugging, and incredibly inconvenient for detecting infinite loops.

I spent an hour. I thought the error was in the comparison code, as that's one place that it'd be easy to get stuck in a loop. I had print statements everywhere. It was yet another horror of coding, and nothing I did helped.

Until I put a print statement at the very end of the targeting code, that is. This entire time, I'd been assuming it was this new code locking up the game. But it wasn't! Debugging told me that the targeting logic was all working. Furthermore, looking at the output showed me exactly what I was doing wrong. I was doing this:

player_ship.target = chosen if chosen

But 'chosen' in this case is a sprite, not a ship. The player_ship is expecting a model object, not a view! All that work, for a six-byte change:

player_ship.target = chosen.model if chosen

Thus, today's Programming Tip of the Day: Don't try to use your view as your model.

Tuesday, June 10, 2008

Day 150: On Automatic

Today I set out to do #178, the in-game quicksave and autosave. Because I already had a utility function that could save any time from anywhere, I figured this'd be a cinch. Create a new control and if it's pressed, save. Turns out, it really was that easy....

My wife came in at this point and asked me what I was doing, so I mentioned that I was working on autosave. She said:

"Oh good, but make it so that it doesn't overwrite the main save file, I hate it when games do that!"

Guess how my save function works! It can't save using any filename other than the universe's name, which happens to be the main save file.

The bulk of that work, then, was fixing that problem. The issue is that if I just tack '-autosave' on to the end of a universe's name and save it, that name persists. So the player, upon loading their autosave and autosaving /that/, gets a name like 'scenario-autosave-autosave.ksg' That would not do.

The save code now looks for two identifiers ('autosave' and 'quicksave'), strips them out, and then adds them back on if you are, in fact, quicksaving or autosaving. I also added in code to put the player's name into the filename as well, so more than one person can play per user account and not have their saves clobber each other. Now if only I could name the player.

I had some extra time, so I got #177 done. That was literally a two-line change, to make some of the editing field bigger. I had time left over.

So, why not, I figured I'd tackle another "easy" task: #160, backporting all the nifty changes to multi-line inputs to the old one-line input.

The plan was, make the one line input a subclass of the multiline, and disallow the creation of new lines. This was easy, but the line was still multi-enabled; it'd wrap if you put too much text in it.

Disabling wrapping was difficult - the MultiLineInput itself delegates the work to the MultiLineLabel. I ended up having to have my subclass override its parent's rendering code entirely (as that's where the wrapping happened), and then set a number of variables that the parent class expected that the rendering would set. It was a pain.

Oh well. At least no global variables have bitten me in the ass.

Monday, June 9, 2008

Day 149: Dep^H^H^HBreadth First Search

It'd be nice if I could implement a Breadth-first search without first accidentally creating a Depth-first search like twelve freaking times.

If the last entry was a blast from the past, this one's an archaeological find. I closed #4, which implements the "Plot Course" button. I did this because the multi-jump tutorial mentions it, and I want everything I mention in said tutorial to be working before I move to the next milestone. This is my new way of putting things off; mentioning them in the tutorial and going "Well, gotta implement this!"

Only this time, I wasn't putting anything off. I've finished #157, and #158, which were the last two editor-intensive parts of the tutorial. So I'm perfectly justified in coding up whatever I feel like.

This blog entry is backwards, because before I did either of those things, I did #176, which lets you display a message to the user once and only once (so they're not constantly pestered with tutorial reminders).

Before I did that, I fixed a bug whereby weapons weren't taking up the right amount of space (their expansion and hardpoints cost was only counted once, regardless of the number of weapons you had). Once again, I discovered that unit testing will only help me if I actually write the tests; I asked myself "Didn't I test for this?" when the bug came up, and the answer turned out to be a big fat "NO!"

And before I did that, I proliferated MiniBuilders to weapons and ammo, which wasn't using them before. At this point I tend to take my editor's stability for granted, so it's very disconcerting when I click a button that's still there because I haven't put a minibuilder in and everything dies.

Finally, the global variable has been quiescent, not biting anything. But I'm watching it....

Friday, June 6, 2008

Day 146: Blast from the Past

Yesterday I finished #168, which prevents the user from seeing (in detail) sectors they haven't been to yet, only to discover that when you add them to your itinerary you still see the names (#175).

Today's subject comes from the fact that, as of earlier today, I had 42 outstanding tickets in milestone:Polish. This is the milestone I throw things in which aren't immediately necessary, but that I want to get done before I ship the game. It's got more tickets in it than any other milestone, because anytime I feel like putting something off, there it goes!

So today I finished #76, the ability to do a quicksave of a universe while you're editing it. This is invaluable because the alternative is to go all the way back to the first editor screen, save, and then immediately load up your scenario again to continue editing.

I had to make two changes in order for this to happen. First, I had to make it possible to respond to a keypress at the engine level; because the editor can be in any one of a dozen states at any time, the engine is the only place they have in common. I was already doing this with the 'kill switch' that shuts the game down (good for testing full-screen!), so I extended it to call arbitrary code in response to any button.

The second change was surprisingly easy. Games-in-progress and scenarios-being-edited, while sharing the same file format, are saved in entirely different ways. I have plans (#169) to widen that gulf even further. So if I'm going to be allowing the person running Kuiper to save the universe they're editing at any time, there needs to be a way to know that the person is, in fact, editing a universe and not playing a game.

Kuiper uses a number of gameplay displays as editors; the map and sector states are examples. Each changes its behavior if it's in "edit mode". A number of other things change if the user is editing. So each thing which cares about edit mode has its edit_mode set to true (usually in its initializer). The problem is, half of the project cares if it's in edit mode! TraderMissions did something similar, but worse; the repository itself was given to everything that needed it, which turned out to be everything. I've done it enough and had it cause enough code bloat to dub it my own personal Anti-pattern: Constantly passing objects that should be global.

So I committed one of the cardinal sins of programming: I made a global variable. It's one of those things you're supposed to do only if you know what you're doing, and usually that rules me out. This time, though, it's making life a lot easier. And that is the story of #174.

The other thing of note I did today was create the KuiSellItemsAction. No, it doesn't force the player to sell items, like I keep thinking when I see it, it puts the items you indicate up for sale on the planets you tell it to. You can sell weapons, cargo, ships, and/or addons. This is useful for, say, simulating technology advances in-game. I use it in the tutorial to only offer weapons to the player after they get the weapons tutorial.

Now taking bets on how long it'll take the global variable to bite me in the ass!

Wednesday, June 4, 2008

Day 144: Your Ship has Come In

So I decided on a whim to cater to that tiny but vocal minority of players who might one day want a ship other than the horrible tiny one they start with. #170 is thus done!

In the fine tradition of making things worse for myself, I then made #171, #172, and #173.

#171 should prevent people from jettisoning mission-related cargo, which is a surprisingly hard fix. The part which displays this is too general to know that it's even displaying cargo, let alone which is mission related. I plan to cheat and have it check to see if its object understands :mission_related, and disallow jettisoning. This also opens things up to one day having missions give the player temporary weaponry and/or addons.

#173 is my pie-in-the-sky ship previewer. When comparing ships, it'd be great to get a side-by-side comparison, complete with how many more (or less) of a given attribute the new ship has. That's easily extended to addons, too.

#172 is, um... making tutorial items actually cost money.

One of my bosses at work has a Staples easy button that says "That was easy!" when pressed.

I'm lobbying to have the voice clip replaced with "That was surprisingly difficult!"

Tuesday, June 3, 2008

Day 143: Addons

First, the huge news: #149 is done!

Okay, that's not huge news. As alluded to before, I've been planning to remove ERB for a while. Today, I wanted to blow another 15 minutes. Thus, an easy ticket (which turned out to actually be easy) was closed.

In more important development related news, #156 was also closed today. That's the Addon tutorial. It took surprisingly long because, as I was testing the tutorial module I asked myself "Self, what if I set out to specifically disregard the tutorial's instructions."

It breaks things, is what!

I had to create a new condition that would check if you'd actually finished a mission, just for this. Now all the tutorial missions are only awarded if you've finished their prerequisite mission. I expect that condition to be handy for multi-mission story arcs, and now you don't have to use flags (which are a pain) to do the same thing.

Finally, I made ticket #170 because it occurred to me that maybe - perhaps - a small minority of players could, at some point in their gameplay experience, want to buy a new ship.

Yeah. I forgot about that until now.

Monday, June 2, 2008

Day 142: Two Hundred Hours

Actually, because I was only a half hour short yesterday, I'm at 201 hours. But still, it's a milestone so I'm naming my blog after it.

Today's work was finishing #166 which means the market dialog is shiny and good and happy. You get details, # of everything it requires that it requires, # of that thing you have free, and the ability to buy/sell more than one of them at a time. I even made it work with weapons, even though that ticket's not up yet.

That's not to say things went extremely well: I had to quash a lot of typos. 15 minutes into some refactoring I decided I didn't like the way it was going, so I ended up reverting all my work up until that point. I kept putting Python syntax in Ruby, thus leading to today's Programming Tip of the Day: To make a new object, use "new".

Still, many things went right. For instance, everything that can be bought and sold has its price stored stored in an instance variable named 'price'. Thank you, me, for making that consistent! Getting weapons to work was literally four lines of code, all of which were copied and pasted from the addons adapter. I got an extra half hour of work in.

Finally, now that the market dialog is handling everything market-related, and the minibuilder and new builder dialog is handling everything build-related, I deleted the old builders. Three entire files (addons.rb, cargo.rb, and weapons.rb) housed nearly identical GUI code that is now handled in one place.

Of course, the downside of this is that, according to my spreadsheet, I wrote -190 lines of code today. I'm still calling it a win.

Sunday, June 1, 2008

Day 141: Slow Ride

No, that's not the title because I took it easy today; it's because my lines of code increased by a whopping 30 over 2.25 hours of work.

Of course, LoC don't tell the whole story - I'm surprised it was positive, as I did some refactoring today and removed a fair bit of code (the old market dialog, for instance). Sometimes the most productive days are the ones where the LoC count goes backwards. That's one of the other ways this differs from Nano.

Today was touchup. #165 required me to put scrollers around the new fancy editors I'd made, and I did it and it was surprisingly easy.

#166, on the other hand, was not so easy. It requires me to have a 'preview' of the cargo/addon/weapon you want to buy. Not a big deal, the preview part was easy. It was the "This requires 1 hardpoint and 2 expansion space, and you have 0 hardpoints and 0 expansions left" part. I essentially made it so any Buyable adapter knows what kind of requirements the things it holds have. The ShipCargoAdapter has "cargo", ShipAddonAdapter will have "expansion" and "hardpoints", etc. These are mapped to actual function names, so I can then dynamically generate the message.

That part's done for the "You have 3 cargo free" part, but not for the requirements. I'm already over my quota for the day, though, so on to tomorrow!

Procrastination for the win!