No pretty screenshots for you today, just walls of text and some confused ramblings. I’m a little out of sorts and not really in any mental shape to be accomplishing interesting things.
I’m feeling a bit un-creative and run down today. So I want to get away from the stuff that requires a lot of creativity (gameplay, writing, art) and just work on something straightforward and mechanical. I’m trying to make sure I do something on the project every day, but it’s probably a bad idea to do anything tricky while I’m all muddle-brained. I check out my miles-long to-do list and find something really dull: reading ini files.
You know ini files. Stuff like this:
[Settings] Setup=1 music=0.40 sound=1.00 fullscreen=0 [Window] Width=1456 Height=887
This is important because right now my ini reader is the only part of my program that’s still tied to Windows. To read and write ini files I’m calling
GetPrivateProfileString () and
WritePrivateProfileString (). Boo. If I ever want to do a Linux port then I need to replace those with my own code.
You might remember that last Christmas I wrote an ini file parser. The good news is that the parser seemed to turn out okay. The bad news is that I was using Qt at the time. Which means I was using the much more useful and powerful Qt string class and not the bog-standard std::string.
std::string is how you’re supposed to use strings in C++. The problem is that it sucks. It doesn’t have a built-in way to strip whitespace off the beginning and end of a string, even though I have never ever written a parser of any sort that didn’t need that. Also, being able to compare two strings in a case-insensitive way is pretty fundamental to most text-processing problems. I need to find the [settings] section of the file even if it’s called [Settings] or [SETTINGS]. Also, I need to convert between numerical values and strings. Qt strings do all of these. std::string does none of them.
So what happens? Everyone needs this stuff, so everyone writes their own string-manipulating tools to cover all these deficiencies. Then everyone has their own little set of string tools, which end up in all the code they write. So now if I want to share my ini reading code with somebody they also need my string tools. They likely have the exact same string tools, and now they have duplicates hanging off my project, cluttering up my code, duplicating their code, causing name collisions, and just generally making life less easy.
I moaned about this on Twitter yesterday and got this reply:
@shamusyoung "case" and lexical sorting are very complicated in I18N. C++ stdlib will never do it well because it can't marry unicode.
— James Iry (@jamesiry) October 8, 2013
@shamusyoung As one example, unicode strings can sort differently based on how they've been normalized. But std::string has no "normalize"
— James Iry (@jamesiry) October 8, 2013
I don’t even know what that means. Unicode still confuses me. I have no idea what “normalizing” a STRING would do. Now, normalizing a vector? That I get. But what does normalizing text do, make all the letters the same height? (I kid.)
I do wonder how Python, Java, and C# do these things, since they don’t have this problem. Regardless of the cause, it’s still a terrible problem that ultimately undermines the strengths of the language. I’ve seen a lot of string tool sets in my lifetime. Most of them were good, a few were great, none of them were complete and all of them were incompatible with each other. Stuff like this is one of the reasons that C++ isn’t nearly as portable as it should be.
It takes time to learn to use a language well. Sure, you can get pretty far in a couple of weeks, but if you’re trying to make a playble, portable, stable, smooth-running game then it requires a depth of knowledge you just can’t get noodling around with example programs. And even if the learning curve wasn’t an issue, the simple conversion time to translate these 40k lines of code would be completely unreasonable.
Don’t be that guy. I’m telling you. That guy is obnoxious.
“Hey Shamus, just use boost!” Yeah, yeah. Of course, I’ve already got my own file-handling code, threading code, a bunch of math, and stuff for juggling strings. Should I re-write all my own stuff to use the more standardized boost library, or maybe just some of them? I mean, I hate to import a massive thing like boost SINCE ALL I WANTED TO DO WAS STRIP SOME SPACES OFF A STRING FOR THE LOVE OF KONRAD ZUSE, ARE YOU KIDDING ME?
So I’ve got this ini-reading code from last year. I pull out the Qt strings, add std:strings, and then plug the holes left by the transition. I run the program and it crashes.
Or at least, I assume it crashes. I hit the run button and the window fails to appear. I assume it silently bombed on startup? A couple of minutes later I realize the thing is still running, it just never got to the part of the program where it created the window. I kill it and then try again, but this time stepping through the code manually. It turns out the problem is that this new code is now the slowest code I have ever written in my entire life. Like, I’ve never seen anything this deeply screwed.
The file I’m reading looks like this:
[Map1] Title=Chapter One Subtitle=The Freezing Caves Fog=0 WallColor=89f LightColor=444 SkyColor=47a StoryPoints=5 6 7 MusicTrack=steady-climb.ogg Xp=150 Tileset=3 Stars=0 Pages=10 SwarmSize=5 Patterns=cave cave cave donut RobotSwarm=cutter1 RobotMooks=worm1 RobotNormal=worm2 RobotBadass=worm3
That is the entire description for level one. The level description file itself is under 300 lines and is ~5k in size. For some danged reason, it takes ONE FULL SECOND to read the above data. That’s like taking eighteen months to make coffee. It’s so far beyond what’s sane or reasonable that I want to delete the parser to punish it for sucking so bad.
The same parser worked reasonably under Qt, and the only change I made was the aforementioned switch to std:string. I’ve heard complaints that std::string is slow, but there’s no way I can blame it for this insane dawdling. I don’t know what to blame, actually. Really, in a single second I should be able to read in the whole file, put every single character in alphabetical order, ROT-13 encode the whole thing, remove all instances of the letter “F”, save it back to disk, and still have nine-tenths of a second left over. What is going on here?
In a fit, I just rip the entire parser apart and re-write it from scratch. I make a fancy version that loads in the file, parses it, and keeps it cached in memory with key values already forced to lowercase for fast lookups. This solves the speed problem and now I can read ini files without using any Microsoft code.
This is actually kind of unfortunate. If I had my head in any kind of working order, I would have run some tests to figure out where the bottleneck was coming from. As it stands, I probably did more work than I needed to, and I destroyed the problem instead of studying it.
So that was my day. I did a ton of work to make a system that was probably overkill for my needs, all to replace a system that used to be a single line of Microsoft-specific code. I have no new features to show for my efforts. I’m not feeling too good about myself right now.
Since today is Post Huge Blocks Of Text Until People Fall Asleep day, let me hit you with another one. I have a reason for all of this text-parsing, and the reason is this:
(Protip: I don’t actually expect you to read this. Just skip to the bottom and I’ll explain.
#Robot template #First is the name of the robot, in brackets. This is the name used to spawn #the robot in the console or in the levels file. [Template] #Ai core defines what kind of logic the bot will use to move. Options are: #* beeline - the bot heads directly for the player in a straight line. #* pounce - the bot attempts to circle around the player, spiraling inward until close, where it "pounces". #* tunnel - the bot loops around the player, passing through level geometry if needed. #* sentry - bot will move until close enough to attack, then root in place. #* orbit - the bot will circle-strafe the player. #* walk - moves along ground. #* hitnrun - Bot heads towards the player when it's ready to attack and away when waiting to refire. AiCore=walk #determines if this is a boss or not. Bosses have their name overhead and special music. Boss=1 #This is the name shown to the user. Only applies to bosses. Name=Walter #BodyParts defines what sprites will make up the robot's body. #If it's a boss, it will use the parts Boss0 through Boss7. #Otherwise it will use parts Robot0 through Robot29 #Use -1 to end the group. #See sprite.ini BodyParts=9 24 23 24 23 21 -1 #This controls how the body parts are arranged. Options are: #* fixed - Single-sprite body. #* snake - Sprites are chained together, with the ending ones chasing after the head. #* squid - Main body has 3 limbs of 2 segments each. #* jelly - Fixed body with one additional sprite hanging below it. Intended to look sort of "jellyfish"-ish. #* worm - Like snake, except the body segments will fall with gravity, collide with ground, and move up and down as the bot moves laterally. #* turret - Uses three sprites: A fixed main body with a rotating arm on either side. BodyType=worm # Size of the head in game units. For reference, player torso is about 0.1 BodySize=0.3 BodyColor=fff #Movement speed Speed=1.9 #How close the player must be before this robot will switch to alerted state. SpotDistance=4 #Eye is the index of the eye used on the head. Eye=1 #This determines how the eye moves around. Options: #sweep, scan, player, heading EyeMovement=player #color of the iris EyeColor=000 #offset of eye from center of head EyeOffset=0 0 #Size of eye relative to head. 0.5 = half head size. EyeSize=0.4 #This is the overpower level of all attacks: Lasers, melee, and missiles. #They correspond to the player levels. Bounce is the number of bounces each laser should do. AttackPower=1 #The distance at which the robot can / should / try to use ranged attacks. #How this number is actually used depends on which AICore is being used. #Defaults to 3 AttackRange=6 #This controls if the robot will lead the player when aiming at them. (Makes them much more dangerous.) #0 = Never #1 = Always #N = Every Nth shot is predicted. AttackPredict=0 #This is used by lasers. Number of wall reflections before the bullet stops. ShotBounce=0 #Number of shots in a single volley of laser fire. 0 to disable lasers. #Refire is time between volleys, in milliseconds. VolleyLaser=5 RefireLaser=2500 #Launcher is the kind of thing to fire. Options are: #basic, homing, cluser, and robot <robot name> Launcher=basic #Volley is how many missiles to fire in a single attack. #Refire is time between volleys, in milliseconds. VolleyMissile=2 RefireMissile=1500 #This is how often (milliseconds) it can hit with melee attack. Set to 0 to disable melee RefireMelee=150 #The MAXIMUM number of each powerup to drop. Defaults to 0. #Actual drop rate is determined by dice rolls and game logic. #If DropMissiles is non-zero, then it will always drop at least 1. DropMissiles=10 DropShields=10 #Voce bank to play when seeing the player, being hit, or dying Voice=2 #Armor is simple damage reduction. Armor=1 Hitpoints=10 # Legs controls how many legs the bot has. (Only work PROPERLY for "walk" AiCores) # WalkHeight is the distance from the ground to the knee. WalkStride is the # lateral distance from the body to the knee. WalkCrouch is how high off the ground # the HEAD is in crouching stance. WalkStand is how high when standing. These values # are expressed in BODY LENGTHS, not world units. Legs=2 WalkHeight=3 WalkStride=3 WalkCrouch=2 WalkStand=5
Okay, the point of all that is that I wanted to move a bunch of the game systems into the config files. It’s now possible to add new robots to the game just by editing the text file. You can then fire up the game and spawn your creation using the console. Or you can open up the level file I showed you earlier and add the bot to the game.
I originally opened up the game like this because a few people said they wanted to play around with modding. Once I was done I realized it was actually a lot nicer to make content for the game like this. I can mess around with game balance, difficulty, and pacing by altering the robot properties and without touching the code.
This also had some odd side-effects. At one point I had a special boss that was just hard-coded to create minions. When I generalized all robot properties I had to include this functionality. So now you can make any bot create other bots. (Instead of firing missiles, they fire robots.) I don’t even know what kind of trouble you can make with that. For fun, I made a robot that “attacks” by creating another robot of the same type. This creates an interesting powers-of-two problem that’s basically unsolvable if you let it go on too long. The robots can’t hurt you, but they can make copies of themselves until your computer runs out of memory, which I think is a kind of victory state for them.
So that’s how today went: I accomplished nothing, but I worked very hard on it.
The true story of three strange days in 1989, when the last months of my adolescence ran out and the first few sparks of adulthood appeared.
Do It Again, Stupid
One of the highest-rated games of all time has some of the least interesting gameplay.
The Death of Half-Life
Valve still hasn't admitted it, but the Half-Life franchise is dead. So what made these games so popular anyway?
Let's ruin everyone's fun by listing all the ways in which zombies can't work, couldn't happen, and don't make sense.
DM of the Rings
Both a celebration and an evisceration of tabletop roleplaying games, by twisting the Lord of the Rings films into a D&D game.