I came extremely close to forgetting I had a blog to write. Fortunately, I got my phone to remind me.
I spent only half of the last week 'working'. The shift in seasons and the sudden dip in temperature always seem to nail me with a case of flu or worse. To top it off, as I'm recovering I suddenly get literal pile of laptops and PCs to fix. Gotta fix them because I need the cash… but that basically knocked two days out of my week. (Speaking of cash, I'm still waiting on the prize money for the Jam I won. Intel is taking their merry time. It's been almost exactly a month since they received my banking details. Have inquired further as to the state of the prizes, along with the other winners).I've actually got a bit of organization planned for these blog posts. So let's get started.
I'm not normally a Sourcetree user, I find the command line to be far easier to quickly bring up and manipulate. But I'm finding the visual branches to be valuable tools for not only aiding with merges but also to see where I am in the project each morning.And that, as it turns out, also helps me figure out what progress to log in this blog! All I have to do is read my week's git status output, or Sourcetree's representation of it, and I know what features I added.I adopt the GIT approach of Commit per Feature. Whether it be something tiny, I add it as a commit. That way I can revert if something becomes too broken, potentially even remove something mid-stream then fast-forward back to Head. So onward to the first big thing I worked on this week.
As pleased as I was with my rapid-fire audio work on the game, listening to the same two tracks and dozen sound effects gets really grating after you've tested the game for the 50th time in a row.Not only that, but big resources make GM's save function as slow as molasses. I'm already at a 25 second save even after removing the audio, and it was nearly three times slower with the audio included.I'll be going down the more sensible route later and loading them externally with an extension.
This covers a somewhat large cut of my git commits, but it was worth it. I finished up the State Machine AI system fully, and have re-implemented my "cannon fodder" enemy with the new system.In addition to the way I described the system last week, I included another structure for entities, an "entity state" structure that has behavioral modifiers for things like movement speed, attentiveness, eyesight, hearing, etc, as these are all things I want my AI to take into account.Somewhere in the middle of working with this I decided that I needed to get rid of the terribly movement I was using for enemies. In the interests of saving time during the jam, I used GM's built in mp_potential_step function. It works great, assuming that nothing is between it and the target, that nothing gets in the way, etc.That basically means it doesn't work at all for what I want.I wanted enemies to be able to at least semi-intelligently pathfind around obstacles to the player (Obviously only if they spotted the player, or 'heard' the player). My first thought was to implement an A* solver. Fortunately, I actually read the manual, and (re)discovered the mp_grid_* functions.I had a couple of wrinkles to iron out using this, notably stopping a path in progress properly, but it's made the enemies suddenly a lot more 'intelligent' than they were before.To counteract their sudden newfound ability to actually track the player down, I implemented the concept of "eyesight" and "attention" in the states.The ai_chase script basically keeps track of whether the entity can see the nearest player entity, and if it can't and is already chasing the player, it starts counting off the "attention" variable. Once attention reaches zero, the state can return a new state (Such as RETURN which can make the entity path back to its former location, or one of the WANDER states I'm working on).So its possible to outrun or hide from an enemy, and each enemy can have different levels of diligence in tracking down the player. The concept of mixing states together has also led to a lot of suddenly easy scenarios to add to the AI. Low level enemies can have a fear response to a player, enemies can emanate a 'call' on seeing the player and have nearby entities come to their aid, nuanced behavior can be introduced via behavior counters, etc.And this system leaves the main Step block looking a lot cleaner than it usually does in my games.
I left, intact, one of my derp comments in there. I leave those to remind me whenever I do something stupid. In this case, I forgot the assignment of state_current in the COOLDOWN case, and spent literally five hours trying to track down what I thought was a horrible design screwup. I should probably make a wrapper script, like "state_execute", that automatically manages reassignment of the current state.As you can see, each of the state scripts takes two arguments at this point. The first is the action to return as 'default', the second is the one to return when the state needs to change. This is a bit limited, since the whole point of states is being able to segue into many potential states from a single state.I'll probably handle this as a per-case scenario, where it makes sense. In my mind, the "look for player" script can only be in two states: Looking and Located.No sense overcomplicating things until I need the complications. Oh, and you might also notice that the state structure has a couple of "register" variables. Could've named them "counter", but register is what my mind jumped to first :PThese are basically just generalized counters used for things like the cooldown state.register_a is set to the desired cooldown, which will vary per enemy, and per state-transition, and it's easier to set one variable than mess around with unique variables, loops and if statements per enemy.Also included is some enemy-specific code that mostly handles animation changes. Kinda necessary, didn't want to complicate that either.
switch(state_current) {
case ENTSTATE.IDLE: {
state_current = ai_look_for_player(ENTSTATE.IDLE, ENTSTATE.CHASE);
sprite_index = spr_enm_remain;
image_speed = 0.05;
} break;
case ENTSTATE.CHASE: {
state_current = ai_chase_player_simple(ENTSTATE.CHASE, ENTSTATE.ATTACK);
sprite_index = spr_enm_remain_walk;
image_speed = 0.05;
} break;
case ENTSTATE.ATTACK: {
state_current = ai_attack_player(ENTSTATE.ATTACK, ENTSTATE.COOLDOWN);
sprite_index = spr_enm_remain_atk;
image_speed = attack_sprite_speed;
if(state_current == ENTSTATE.COOLDOWN) {
state_register_a = 10;
}
} break;
case ENTSTATE.COOLDOWN: {
state_current = ai_action_cooldown(ENTSTATE.COOLDOWN, ENTSTATE.IDLE); // First major derp
sprite_index = spr_enm_remain;
image_speed = 0.05;
} break;
default:
state_current = ENTSTATE.IDLE;
break;
}
In preparation for the next item on the list, I implemented a system for saving/loading configuration data. In a bunch of older games I've made, I just used the usual GM INI file functions and stored things as plain text, which I think is fine for certain parameters that I believe should be human-editable (Resolution, scaling, fullscreen, etc).But then I decided that with the relatively complex set of options I want available for input, I'd probably do a bit better by making an external option-editing tool for the game ala Spelunky. This also helps if the default graphical setup doesn't work for some reason or other.So, to make the ini configs and make my internal code a bit clearer, I just save everything in a map (I'm using those a lot in this project), then just write that to a string, and load it when needed. Currently the only two options that are tracked (Besided input, as mentioned in the next section), are what window scale the game is using, and whether fullscreen is to be used or not. It remembers your last setting.
Input is something I consider a core-mechanic to any game, and in this case I pretty much threw the game together in a panic and set default mappings of "Arrow keys and ZXCV". That's all well and good for people who like that mapping… most don't. So I decided to get the potential nasty business of custom input mapping out of the way now rather than deal with it later.Thinking ahead to a potential feature, I also decided to add support not just for a gamepad, but for up to four of the things.This also led to players now keeping track of their player id and an input id. The eventual plan is to maybe allow for chaotic four-player local play with gamepads or keyboard.Implementing the remapping is (relatively) simple, and I just store the results in the config map with action keys + controller id. So something like this:
Then, with a few custom input scripts, the game prefixes the action supplied to the script with whatever controller the calling player has mapped to them. Gamepads are taken into special consideration; I'm applying the standard XB360 mapping with options to remap the face button actions. I have yet to implement a nice way to change the mappings, but that'll be for later. Right now it's just a good thing that I have the system, and I won't have to go do a nightmare search-replace session later to add support for it.
kbd_attack,
kbd_item,
kbd_left,
gp_action,
gp_attack
etc