DSR Log #1: Post Jam Fixup

Posted by Mega on April 21, 2016, 10:20 a.m.

You know what I never seem to do?

Work on the same project for longer than a week. Kinda a problem I've had for ages.

But I've managed, over time, to discipline myself and learn to ignore the annoying little voice in my head that tells me that every new idea I have is what I should be working on.

So, my game that won me first place in the locally-hosted NAGJam has been the focus of my attention since I submitted, and I intend to keep it that way. I want to take this through to completion, put it up for a couple of bucks and take that experience forward into my next project.

And for once, I don't have a definite "next project". I'm actively discarding ideas at this point, keeping a laser-like focus on my current work.

Hopefully I'll follow through on this; I really need somebody to stand behind my chair and slap me alongside the head when I get sidetracked.

I'm going to attempt to write a semi-regular (Possibly weekly) log of development on the game, along with any problems I encounter, tricks I discovered, the occasional beta release…

I do find that writing about your plans regularly, telling other people about them and showing them the unfinished product tends to instill a need to finish said product or face the consequences (Awkwardly telling people that you're kinda not working on that thing you were so hyped for a week ago.)

The Game at a Glance

To keep things simple, the game is currently a very small hack 'n slash action game.

You run from the starting room into the boss room, die, return and try again until you win.

After that, you get booted back to the title screen after a congratulatory message.

What the game was meant to be

I'm not going to lie, my entire thought process going into the game was basically: Can I translate Souls style gameplay into a simple top-down representation, ala Gauntlet?

Answer: Yes, but I didn't get that far. Or at least, the demo didn't.

Some of the original trappings I discarded were things like random world generation (Always a dumb idea for a game jam), Diablo style enemies and boss groups (Too complicated for a jam), and local co-op (Would have been nice, but again, JAM).

At this point I've added all of these to my to-do list for post-production (Stuff to add after the game is feature complete).

The Plan

Currently, I have a clear idea of where I want the game to be, and have it down into a long list of small to medium size features that I can implement easily in chunks; this is a design method that definitely works (For me), and has been leading to a sizable list of "completed" goals, which is encouraging.

My goal is to keep working like this, adding necessary features to my game until I reach a point where I need to branch out into "world building". This is where things get tricky and I need to come up with the path of the actual game, basically "where does the player fit into things and what is their goal?".

Also, dozens of enemy sprites.

The Engine

I toyed briefly with the idea of moving the game over to MonoGame, but decided against it over reasons of efficiency.

Yes, it definitely would be nicer to work in an actual programming language, especially one with classes and proper scoping, but the process of moving and setting up the game in a new environment would cost me weeks in which I could be actually making the game in the existing setting.

Game Maker is far from ideal as far as toolsets go, it has severe limitations to performance and capability. But it's still adequate for my purposes.

My justification is that while Language and Framework X has a list of nice features, I don't really need them to complete my game.

GM is full of quirks and things that I need to work around in order to achieve my desired results at times, but they are, at least achievable.

I've also learned a few handy tricks to make things easier to work with too, as detailed below.

Working with GM:S

The game, as I want it, is basically a big stat-fest. Everything in the game is tied to stats, attributes and states in some way or other, and this gets difficult to manage… if you're doing things in the usual way.

Thinking ahead to when I'll need ways to not only save and load state but potentially share it across games (Think online co-op), I've opted to save any and all state in ds_map structures.

To make things easier to manage from a single location, I have created several scripts that handle different "classes" of data.

Here's an excerpt from one of them:

///sc_create_stats()

_struct_stat_table = ds_map_create();

stat_hp = 0;
stat_hp_max = 0;
stat_hp_refresh = 0;
stat_st = 0;
stat_st_max = 0;
stat_st_refresh = 0;

stat_str = 0;
stat_dex = 0;
// etc...

I know it looks like I"m basically assigning a bunch of 'normal' vars up there, but in fact those special.

I'm leveraging the macro system that GM:S has to not only make my stat maps easier to access, it also allows for auto-complete lookup which is invaluable when you have over 70 total attributes and stats to try remember.

Defining the macros in GM's internal editor is a pain in the ass though.

Fortunately, there's a better way: Directly editing the project file.

GM:S stores everything as a simple XML document.

Macros are stored under the "constants" element as simple tags.

So here's a few of mine:

<constants number="34">
    <constant name="stat_hp">_struct_stat_table[? "hp"]</constant>
    <constant name="stat_hp_max">_struct_stat_table[? "hp_max"]</constant>
    <constant name="stat_hp_refresh">_struct_stat_table[? "hp_refresh"]</constant>
    <constant name="stat_st">_struct_stat_table[? "st"]</constant>
    <constant name="stat_st_max">_struct_stat_table[? "st_max"]</constant>
    <constant name="stat_st_refresh">_struct_stat_table[? "st_refresh"]</constant>
    etc...
</constants>

The only annoyance, and one I can't exactly see the need for, is that you have to manually increment the number attribute each time you add a new macro.

It's not exactly difficult to dynamically load all of the elements of a tag in .NET (Which GM:S uses). Or find out the exact number of elements in code.

Anyway, macros expand before compilation, basically acting as a global search/replace and substituting the macro's name for whatever it contains.

In this case, stat_hp expands to _struct_stat_table[? "hp"] (That's Accessor syntax if you didn't know it, basically a shortcut for accessing data structures in GM).

This basically allows, for both retrieval and storage, to just type in stat_, then choose from the list that pops up.

Git

Game Maker's backup system slows everything down ridiculously, given that it copies hundreds of individual little files each time you save or compile. Save times get horrendously slow the more objects you have, no matter what they are.

So I disabled the "backup on save" and "backup on compile" options under preferences and set up a local Git repo; this leverages the fact that basically everything in GM is now stored as a separate XML file, and allows me to "rewind" changes made, make use of branching for feature development and generally do things a lot faster.

Also have a simple remote repo set up on my laptop for off-PC backups.

Type Hierarchy

I mentioned this in my last blog about this jam, but I've basically implemented a "typing" system that utilizes GM's parenting system to give objects behaviors and traits.

I'm currently moving behaviors out of that into their own scripts (Specifically, things that are repetitive and can be used by many entities), and then just building up behaviors in the step event. Since all objects have the same 'state', it means that the behaviors are basically universally compatible.

// Create Event
sc_create_stats();
sc_create_object_state(); 

// Step Event
switch(game_object_state) {
case STATE.IDLE:
    game_object_state = sc_ai_check_area();
    break;
case STATE.CHASE:
    game_object_state = sc_ai_chase_entity(game_object_target);
    break;
case STATE.MELEERANGE:
    game_object_state = STATE.ATTACK;
    break;
case STATE.ATTACK:
    sc_entity_attack(game_object_target, self.weapon);
    game_object_state = STATE.COOLDOWN;
    break;
case STATE.COOLDOWN:
    game_object_state = sc_ai_action_cooldown(stat_atk_cooldown, stat_atk_cooldown_max);
    break;
default:
    game_object_state = STATE.IDLE;
};

This is all still very simple, not deviating too much into the possibilities.

The idea here is, eventually, to introduce more complex behaviors via state parameters (Or function arguments), allowing for slight or not-so-slight variations in behaviors, eventually leading to an easy-to-use system for creating different enemy types.

Item/Weapon instances

Another slightly annoying to implement feature was weapons and items. These can have any range of effects and side-effects on players and enemies, and I needed an easy way to access and manage them, preferably also having them accessible via auto-complete.

The way I did this was to create an instance type (wep_base) that contains weapon attributes (Another map type), as well as types for other "equipment slots" (There are only two at the moment). These are part of the object state, and any entity that has a state can have items or weapons equipped.

The weapons themselves handle their own use via custom events, as well as their own rendering and side effects (So no massive if nests in the player/enemy objects).

Using the weapon in player or enemy code is as simple as calling sc_entity_attack() which does a quick check that the player has a weapon assigned, then executing the event in the context of the object instance.

The entire system is far simpler than I make it sound, and allows for a lot of subtle variation that will allow for rapid addition later of player/enemy equipment. I thought it'd be fun for enemies to also be able to leverage the same 'abilities' the player has, and this will also tie into the Diablo 2 style enemy/boss groups later on in the development cycle.

Final Word

This was one looong writeup. That's probably because I've been working on this for nearly a month, and had a lot of catchup-writing to do. Next entry shouldn't be anywhere near as long, assuming I don't go adding a dozen new major features between now and next Thursday.

Comments

Zuurix 7 years, 12 months ago

Yep, writing about the game weekly really helps.

I don't have problems with cancelling games, but I do more progress this way. Also it reminds people that your game exists =P

Aside from that…

Quote: GM documentation

A macro is, as the name implies, something that can hold a constant value (real, or boolean) or a string, or an expression. This is different to a variable in that it can't be changed once defined.

Mega 7 years, 12 months ago

In computing in general:

Quote:
a single instruction that expands automatically into a set of instructions to perform a particular task.

Macros have been around for decades, and that's the accepted usage. The thing is, constants can only generally evaluate to a single result at compile time. In programming, C and C++ specifically, macros "frame" an expression.

So:

#define MY_MACRO(A) (A*A)

Expands in code, always, to "A*A", but the framed statement (A) can change over the course of runtime.