Pages: [1]
  Print  
Author Topic: RPG Maker-style games in Game Maker  (Read 9279 times)
Offline (Unknown gender) luiscubal
Posted on: February 20, 2011, 12:46:19 PM
Member
Joined: Jun 2009
Posts: 452

View Profile Email
**Split into multiple posts because it exceeds the forums max size limit**

For quite some time, I've used GM for platform and puzzle games. However, I never used GM to make RPGs. I wanted to change that. Here's how I implemented a basic RPG engine in Game Maker. Much of this design can easily be ported to SDL, SFML and other APIs.

Before getting started
1. Make sure you have Game Maker installed. I don't think ENIGMA has the required functionality to run this yet.
2. Make sure you have some knowledge of GML, as I use it heavily.

Getting Started
The goal is to have a "classic" 2D RPG. I will not be covering random encounters here and the game will have no battles. Just moving around the world
The world must be organized in 32x32 tiles.
Each instance is precisely located in one of these tiles. However, when moving between tiles, the sprite should have a moving animation and gradually move between tiles, rather than instantly "jump". For all collision detection purposes, however, each instance is always in one of these tiles - only the drawing subsystem moves gradually.
The system should nicely fit with windows and pauses. This means freezing all movement on pause and some kind of "keyboard focus" system.
We want to apply the DRY(Don't Repeat Yourself) principle to this engine, so we'll be heavily using base classes("parent objects" in GM).
The engine reserves two base objects - "actor_obj" and "window_obj".
An Actor is an instance that is located in the world and interacts with other actors. An actor can be the main game hero, NPCs, doors, rocks and walls. It is important to know that only actors exist in the collision system.

Because of our very specific requirements, we will not be using GM's built-in hspeed/vspeed nor collision engine. Also, no object in our game will be "solid". We will be implementing our own hspeed/vspeed/collision engine.

Since only actors exist in the collision engine, the first thing we'll need is a function("script") to find if a given instance is an actor. We'll soon need a similar function for windows, so we'll have multiple scripts as a way to ensure compliance with DRY.

bool instance_is_actor(instance)
Code: ( (Unknown Language)) [Select]
return object_type_is_actor(argument0.object_index);
bool object_type_is_actor(object_type)
Code: ( (Unknown Language)) [Select]
return object_is_ancestor(argument0, actor_obj);
Now, we'll go to the actor_obj object. Our actor_obj is essentially a big part of our DRY plan, since we'll put all common stuff here. actor_obj is not meant to be used directly and, instead, should be seen as an abstract base class.
We will require several variables in actor_obj, related to the multiple features we want to have.
We are going to put sensible defaults for some of those variables in the Create Event.

First of all, we have to address the problem of mismatch between draw(GM) location and collision engine location.
For that, we will create the "target_x" and "target_y" variables. In addition, we will have to know when to stop("!moving") and where we're heading to("orientation").
Also, different actors may move at different speeds.

actor_obj Create Event
Code: ( (Unknown Language)) [Select]
self.target_x = x;self.target_y = y;self.moving = false;self.orientation = 0;self.motion_speed = 2; //In tiles per second
We will need some sort of convention for orientation. I used down=0, left=1, right=2, up=3. Choosing this order has the nice property that 3 - orientation equals the inverse orientation.

One important part of our engine is in the step event.
In our step event, we will have to move our character if needed and also we'll have to stop it when the motion is complete.

actor_obj Step Event
Code: ( (Unknown Language)) [Select]
if (self.moving) {        if (orientation == 0) {        self.y += self.motion_speed * 32 / room_speed;    }    if (orientation == 1) {        self.x -= self.motion_speed * 32 / room_speed;    }    if (orientation == 2) {        self.x += self.motion_speed * 32 / room_speed;    }    if (orientation == 3) {        self.y -= self.motion_speed * 32 / room_speed;    }        if (self.x mod 32 == 0 && (self.orientation == 1 || self.orientation == 2)) {        self.moving = false;        self.target_x = x;    }        if (self.y mod 32 == 0 && (self.orientation == 0 || self.orientation == 3)) {        self.moving = false;        self.target_y = y;    }}
One important thing to note is that we're trying to be somewhat independent from room_speed. However, it is possible that this results in rounding trouble, so I recommend using a power of 2 room_speed for your rooms, such as 16 or 32.
I have tested this engine with 32.
Here we move as much as needed, and check if we arrived to wherever we want to arrive.
However, notice we don't yet have a good way to "start moving". In particular, we can't just set the moving variable since we also need to update target_x/target_y.
In the name of DRY, we'll isolate "start moving" in a separated method - "perform_motion":

void perform_motion()
Code: ( (Unknown Language)) [Select]
self.moving = true;self.target_x = x_in_direction(self.orientation);self.target_y = y_in_direction(self.orientation);
real x_in_direction(direction)
Code: ( (Unknown Language)) [Select]
if (argument0 == 1)    return x - 32;if (argument0 == 2)    return x + 32;return x;
real y_in_direction(direction)
Code: ( (Unknown Language)) [Select]
if (argument0 == 0)    return y + 32;if (argument0 == 3)    return y - 32;return y;
Now, let's say we want to have something to test.
We're going to create our room(room_speed=32), and we'll want in it an instance of hero_obj. It is important to always place actors in x/y that are multiple of 32. GM room editor's grid will certainly help here.
So, first we'll be creating our sprite. Put *anything* there, just to test it. Remember, however, that the engine expects a 32*32 actor. If you pick a different size, you may also want to take a look at the sprite origin.
Now, our hero_obj will have actor_obj as parent.
The first thing we'll want to have is our hero move when the player presses one of the directional keys.
Again, we want to abide by the DRY principle, so we'll want as much of the 4 keys code to be shared.

What happens when the user presses a directional key?
If the hero is moving, nothing happens.
If the hero is stopped and looking elsewhere, then the hero should look at the direction of the key.
If the hero is stopped and looking at the pressed direction, then the hero should start moving in that direction.
So we'll implement this:

void perform_dirkeypress(direction)
Code: ( (Unknown Language)) [Select]
if (!self.moving) {    if (self.orientation == argument0)        perform_motion();    else        self.orientation = argument0;}
Now, we'll have to implement keyboard events(Keyboard, NOT KeyPress). It'll be very simple KeyDown=perform_dirkeypress(0), KeyUp=perform_dirkeypress(3), etc.
In addition, we don't necessarily have to use the directional keys. We can instead decide to use the ASDW keys, for example.

Now, it's time to test our game.
Our lonely hero should be moving across an empty room in whatever direction you tell it to go.
« Last Edit: February 20, 2011, 07:44:31 PM by luiscubal » Logged
Offline (Unknown gender) luiscubal
Reply #1 Posted on: February 20, 2011, 12:46:53 PM
Member
Joined: Jun 2009
Posts: 452

View Profile Email
Animating the sprites

You will soon notice that having a single sprite for the hero won't do it.
Instead, we want 4 sprites - one for each direction the hero can be facing.
We could change the sprite on the key events, but we also want the facing to properly change to fit the direction. The hero should look down when self.orientation=0, up when self.orientation=3, etc.
However, not *all* actors will have this. Some(notably objects such as boulders) might always be facing the same direction, so we'll need a new variable here: oriented_sprite=true/false.
Because there is no sensible default for this option, each actor should set its own.

hero_obj Create Event
Code: ( (Unknown Language)) [Select]
event_inherited();self.oriented_sprite = true;
We'll now be editing the actor_obj:

actor_obj Create Event
Code: ( (Unknown Language)) [Select]
self.target_x = x;self.target_y = y;self.base_sprite = sprite_index;self.moving = false;self.orientation = 0;self.motion_speed = 2;
actor_obj Step Event
Code: ( (Unknown Language)) [Select]
self.sprite_index = self.base_sprite;if (self.oriented_sprite)    self.sprite_index += self.orientation;if (self.moving) {    if (orientation == 0) {        self.y += self.motion_speed * 32 / room_speed;    }    if (orientation == 1) {        self.x -= self.motion_speed * 32 / room_speed;    }    if (orientation == 2) {        self.x += self.motion_speed * 32 / room_speed;    }    if (orientation == 3) {        self.y -= self.motion_speed * 32 / room_speed;    }        if (self.x mod 32 == 0 && (self.orientation == 1 || self.orientation == 2)) {        self.moving = false;        self.target_x = x;    }        if (self.y mod 32 == 0 && (self.orientation == 0 || self.orientation == 3)) {        self.moving = false;        self.target_y = y;    }}
For this to work, it is important that the sprites have consecutive IDs, in the proper order.
It is also important to ensure that the object initial sprite(as set in the object editor) to be the sprite looking down.

Our game now looks and feels better. But there's still something awkward. The hero just "slides" through the world.
It would be better if the characters would have a step animation.
However, when should the step animation kick in?
We will have three modes for this:
0 - NEVER have the moving animation, always hold still
1 - ALWAYS have the moving animation
2 - Only have the moving animation WHEN MOVING.

Because there is no sensible default for this option, each actor will have to set it independently.
Also, we will want to control the animation speed(which too has no sensible default)
So we're going to change the hero create event to set these new variables:

hero_obj Create Event
Code: ( (Unknown Language)) [Select]
event_inherited();self.animation_speed = 6; //In images per secondself.animation_mode = 2;self.oriented_sprite = true;
Now, we're going to change the actor_obj to use these new variables:

actor_obj Step Event
Code: ( (Unknown Language)) [Select]
self.sprite_index = self.base_sprite;if (self.oriented_sprite)    self.sprite_index += self.orientation;if (self.moving) {    if (self.animation_mode == 0)        self.image_speed = 0;    else        self.image_speed = self.animation_speed / room_speed;        if (orientation == 0) {        self.y += self.motion_speed * 32 / room_speed;    }    if (orientation == 1) {        self.x -= self.motion_speed * 32 / room_speed;    }    if (orientation == 2) {        self.x += self.motion_speed * 32 / room_speed;    }    if (orientation == 3) {        self.y -= self.motion_speed * 32 / room_speed;    }        if (self.x mod 32 == 0 && (self.orientation == 1 || self.orientation == 2)) {        self.moving = false;        self.target_x = x;    }        if (self.y mod 32 == 0 && (self.orientation == 0 || self.orientation == 3)) {        self.moving = false;        self.target_y = y;    }}else {    if (self.animation_mode == 2)        self.image_index = 1;        if (self.animation_mode == 1)        self.image_speed = self.animation_speed / room_speed;    else        self.image_speed = 0;}
It is important to note some design details.
This code assumes that the animation goes like this:
image 0: step 1
image 1: stop
image 2: step 2
image 3: stop

We need code to set the correct speed and index depending on the motion mode, so we had that extra code.
Our game should now look and feel better again.

Collisions and walls

One critical element of RPG games is the collision system.
Unfortunately, the collision system in GM doesn't fit our needs, so we'll implement our own very simple system.

First of all, not all actors collide. Some actors may allow other actors to move on top of them.
So we'll need yet another variable: collision_enabled.
Again, there's no sensible default for this variable, so each actor will have to specify it.

hero_obj Create Event
Code: ( (Unknown Language)) [Select]
event_inherited();self.animation_speed = 6;self.animation_mode = 2;self.oriented_sprite = true;self.collision_enabled = true;
Now, we'll edit perform_motion to consider this:

void perform_motion()
Code: ( (Unknown Language)) [Select]
if (can_move_in_direction(self.orientation)) {    self.moving = true;    self.target_x = x_in_direction(self.orientation);    self.target_y = y_in_direction(self.orientation);}
We'll need the function can_move_in_direction now.

bool can_move_in_direction(direction)
Code: ( (Unknown Language)) [Select]
return !self.collision_enabled || !collision_at(x_in_direction(argument0), y_in_direction(argument0));
bool collision_at(x, y)
Code: ( (Unknown Language)) [Select]
var i;for (i = 0; i < instance_count; i += 1) {    var inst;    inst = instance_id[i];    if (instance_exists(inst)) {        if (instance_is_actor(inst)) {            if (inst.collision_enabled && inst.target_x == argument0 && inst.target_y == argument1)                return true;        }    }}return false;
If you have the luxury of using a language with short-circuited logical operators, you'll be able to improve the look of the code above.

So, we're going to create a new actor: wall_obj.
Depending on your needs, you might to set visible=false for this actor and use tiles for theming instead. Or maybe not, it's up to you to decide.
Remember to set parent object to actor_obj

wall_obj Create Event
Code: ( (Unknown Language)) [Select]
event_inherited();self.animation_mode = 0;self.oriented_sprite = false;self.collision_enabled = true;
Again, when placing walls in the room, remember to ALWAYS put them in multiple of 32 positions.

Now we have a collision system in place. We can now decide where the hero can go to, etc.
Logged
Offline (Unknown gender) luiscubal
Reply #2 Posted on: February 20, 2011, 12:47:53 PM
Member
Joined: Jun 2009
Posts: 452

View Profile Email
Actor interaction

The next thing to do is have the actors interact with each other.
We will define three types of interactions: Action, Touch and Overlap

We will use "Action" when we want to "talk" with actors, "open" chests, etc.
We will use "Touch" when we want to trigger an event when two actors touch each other. One example is having moving boulders.
We will use "Overlap" when an actor walks to the top of another actor(where at least one of the actors obviously must have collision_enabled=false).
One use of overlap is when the actor steps in the door and is moved to a different room, or maybe another actor notices the actors and comes running to it saying that he forgot something.

Notice that all of these events require two actors. We will use a new variable "evt_other" for this.

To implement these events we will use user events. 0 for Action, 1 for Touch and 2 for Overlap.
Let's start with Overlap, which is the easier to implement.

void perform_overlap_event(actor)
Code: ( (Unknown Language)) [Select]
var inst_id;inst_id = self.id;with (argument0) {    self.evt_other = inst_id;    event_user(2);}
void perform_overlap_in_position(x, y, actor)
Code: ( (Unknown Language)) [Select]
var i;for (i = 0; i < instance_count; i += 1) {    var inst;    inst = instance_id[i];    if (instance_exists(inst)) {        if (instance_is_actor(inst)) {            if (inst.target_x == argument0 && inst.target_y == argument1) {                perform_overlap_event(inst, argument2);            }        }    }}
Now, we only need to edit actor_obj:

actor_obj Step Event
Code: ( (Unknown Language)) [Select]
self.sprite_index = self.base_sprite;if (self.oriented_sprite)    self.sprite_index += self.orientation;if (self.moving) {    if (self.animation_mode == 0)        self.image_speed = 0;    else        self.image_speed = self.animation_speed / room_speed;        if (orientation == 0) {        self.y += self.motion_speed * 32 / room_speed;    }    if (orientation == 1) {        self.x -= self.motion_speed * 32 / room_speed;    }    if (orientation == 2) {        self.x += self.motion_speed * 32 / room_speed;    }    if (orientation == 3) {        self.y -= self.motion_speed * 32 / room_speed;    }        if (self.x mod 32 == 0 && (self.orientation == 1 || self.orientation == 2)) {        self.moving = false;        self.target_x = x;    }        if (self.y mod 32 == 0 && (self.orientation == 0 || self.orientation == 3)) {        self.moving = false;        self.target_y = y;    }        if (!self.moving) {        perform_overlap_in_position(self.x, self.y, self);    }}else {    if (self.animation_mode == 2)        self.image_index = 1;        if (self.animation_mode == 1)        self.image_speed = self.animation_speed / room_speed;    else        self.image_speed = 0;}
Now, all you have to do to test this is create a collision_enabled=false actor and move the hero on top of it.
Note that when doing this, you might want to check if evt_other.object_index==actor_obj, as this is not always the case. All actors trigger overlap events, which might have unintended consequences(such as NPCs triggering events that only the hero was supposed to trigger).

So we have our first kind of event implemented.
The second type of event we'll want is the "Action Event". Go to the hero_obj and create a Press Space event(KeyPress, not Keyboard):

hero_obj Press Space
Code: ( (Unknown Language)) [Select]
var fa;fa = get_faced_actor();if (fa >= 0) {    fa.orientation = invert_orientation(self.orientation);    fa.evt_other = self.id;    with (fa) {        event_user(0);    }}
Now we have quite a few new functions to implement.
First, we'll get the actor the hero is facing.
If it exists, we'll change its orientation to be facing the hero, and trigger the action event.

real invert_orientation(orientation)
Code: ( (Unknown Language)) [Select]
return 3 - argument0;
instanceid actor_at_orientation(orientation)
Code: ( (Unknown Language)) [Select]
return actor_at_position(x_in_direction(argument0), y_in_direction(argument0));
instanceid actor_at_position(x, y)
Code: ( (Unknown Language)) [Select]
var i;for (i = 0; i < instance_count; i += 1) {    var inst_idx;    inst_idx = instance_id[i];    if (instance_exists(inst_idx)) {        if (instance_is_actor(inst_idx)) {            if (inst_idx.target_x == argument0 && inst_idx.target_y == argument1)                return inst_idx;        }    }}return -1;
Now, you can just create your NPCs and have your hero talk to them. Try it out.

Our final event type is "Touch". We'll want to implement some sort of pushing boulders system(if you ever played Pokemon, think HM Strength).

This event will be very simple. We'll start by modifying perform_motion:

void perform_motion()
Code: ( (Unknown Language)) [Select]
if (can_move_in_direction(self.orientation)) {    self.moving = true;    self.target_x = x_in_direction(self.orientation);    self.target_y = y_in_direction(self.orientation);}else {    perform_touch_in_direction(self.orientation);}
void perform_touch_in_direction(direction)
Code: ( (Unknown Language)) [Select]
perform_touch_in_position(x_in_direction(argument0), y_in_direction(argument0), self);
void perform_touch_in_position(x, y, actor)
Code: ( (Unknown Language)) [Select]
var i;for (i = 0; i < instance_count; i += 1) {    var inst;    inst = instance_id[i];    if (instance_exists(inst)) {        if (instance_is_actor(inst)) {            if (inst.target_x == argument0 && inst.target_y == argument1) {                perform_touch_event(inst, argument2);            }        }    }}
void perform_touch_event(actor)
Code: ( (Unknown Language)) [Select]
var inst_id;inst_id = self.id;with (argument0) {    self.evt_other = inst_id;    event_user(1);}
Now, how to use this?
I'll suggest creating a boulder sprite, and a rock_obj object.
As usual rock_obj will have actor_obj as parent.

rock_obj Create Event
Code: ( (Unknown Language)) [Select]
event_inherited();self.animation_mode = 0;self.oriented_sprite = false;self.collision_enabled = true;
rock_obj User Defined Event 1 (Touch)
Code: ( (Unknown Language)) [Select]
self.orientation = self.evt_other.orientation;perform_motion();
Now, try it.

One thing you may notice is that when the hero pushes a boulder that has another boulder behind it, both boulders move it.
This happens because the boulder that's being moved too sends a touch event to the boulder behind it.

To prevent this from happening, we simply check who is sending the touch event.
rock_obj User Defined Event 1 (Touch)
Code: ( (Unknown Language)) [Select]
if (self.evt_other.object_index == hero_obj) {    self.orientation = self.evt_other.orientation;    perform_motion();}
Now, if the hero tries to push a boulder with another boulder behind it, the boulder will not move.
Remember that if you have actors with overlap events, the moving boulders will too trigger them, so you might want to perform similar checks in those events.
Logged
Offline (Unknown gender) luiscubal
Reply #3 Posted on: February 20, 2011, 12:48:11 PM
Member
Joined: Jun 2009
Posts: 452

View Profile Email
Windows and Messages

Let's say our hero is happily exploring a brand new world when he meets a friendly(or not-so-friendly) NPC. We will now want to talk to him to hear the helpful(or not-so-helpful) tips he will surely give.
So our NPC has an Action event but... what are the contents of that event?
We will introduce a window/input focusing system.

Again, we'll create an abstract base class object. Like actor_obj, this object will have NO parent.
And again, we'll need some helper functions:

bool instance_is_window(instance)
Code: ( (Unknown Language)) [Select]
return object_is_ancestor(argument0.object_index, window_obj);
The first important thing to notice is that some windows "steal focus", while others do not. For instance, is a window steals focus, the player will be unable to use the direction keys to make the hero move.
To control this, we'll have a variable named steal_focus.

window_obj Create Event
Code: ( (Unknown Language)) [Select]
self.steal_focus = true;
Now, multiple windows may be open, and each might be stealing focus from each other. So we'll need to solve this.
Our convention is: Only one window can have focus at a time. A window has focus if and only if there are no windows with greater IDs stealing focus from it.
This definition might not be perfect, but it's a good start and, essentially, it is very simple to understand and implement. And, in fact, in most cases, it will be what one would normally expect.
So we manage to get our windowing system to respect both the KISS(Keep it simple, Stupid!) and PLA(Principle of Least Astonishment) principles.
As for actors, an actor has focus if there are zero windows stealing focus. We implement this by pretending that the actor is a window with a very low ID(-1).

So, on all our keyboard and key press events, we'll add this to the beginning:
Code: ( (Unknown Language)) [Select]
if (!has_focus(-1))    return 0;
bool has_focus(window_id)
Code: ( (Unknown Language)) [Select]
var i;for (i = 0; i < instance_count; i += 1) {    var inst;    inst = instance_id[i];    if (instance_exists(inst) && instance_is_window(inst)) {        if (inst.steal_focus && inst > argument0) {            return false;        }    }}return true;
Now, we'll need one very specific type of window: A message window.
Message windows will contain text and, in this specific case, have minimal styling.
We'll create a message_obj with window_obj as parent.
We will want the space key to close(destroy) the message window, but usually it is the space key that opens it in the first place.
So we'll have to ensure that at least one frame goes between the window opening and the window closure.

message_obj Create Event
Code: ( (Unknown Language)) [Select]
event_inherited();self.message_text = "";self.position = 1; /*0=top, 1=bottom*/self.message_height = 120;self.steal_focus = true;self.first_frame = true;
message_obj End Step Event
Code: ( (Unknown Language)) [Select]
self.first_frame = false;
message_obj Press Space
Code: ( (Unknown Language)) [Select]
if (!has_focus(self.id))    return 0;if (!self.first_frame)    instance_destroy();
Notice that we're using has_focus with the ID instead of has_focus with -1. This is because this time we're actually checking the focus of a window instead of an actor.
But this still does not show the message on the screen.
For that, we need the Draw Event.

message_obj Draw Event
Code: ( (Unknown Language)) [Select]
var by;if (self.position == 0)    by = 0;else    by = room_height - self.message_height;draw_set_color(c_white);draw_rectangle(0, by, room_width, by + self.message_height, false);draw_set_color(c_black);draw_text(5, by + 5, self.message_text);
In this case, we do not yet support views. Adding support for views shouldn't be too hard, though.

Now, we will want an utility function that we call to show a message.

instanceid show_game_message(text)
Code: ( (Unknown Language)) [Select]
var inst;inst = instance_create(0, 0, message_obj);inst.message_text = argument0;return inst;
Now, we just have to call this function to show messages on the screen. This should greatly improve conversations with NPCs.
Worth noting that right now, you have to be careful about line breaks and add them yourself to string passed as argument of show_game_message.

Sometimes, we want to wait until a window is closed and then do something(for instance, showing two consecutive windows).
For that, we'll need a new event and some changes:

window_obj Create Event
Code: ( (Unknown Language)) [Select]
self.steal_focus = true;self.owner = -1;
Now, if the window has an owner, we have to notify it using the user event 3.

window_obj Destroy Event
Code: ( (Unknown Language)) [Select]
if (self.owner != -1) {    self.owner.evt_other = self.id;    with (self.owner) {        event_user(3);    }}
Finally, we must ensure the owner is correctly set for every window.

instanceid show_game_message(text)
Code: ( (Unknown Language)) [Select]
var inst;inst = instance_create(0, 0, message_obj);inst.message_text = argument0;inst.owner = self.id;return inst;
This is it! Everything is done and now the window owner should be notified when the window is closed.

Scenes and Action Sequences

What if we want to code a sequence of actions to an actor? Like "Move down twice, then move right".
Right now, this does not require any kind of change to the engine because it already supports this.

Let's start by adding a new variable in the Create Event to whatever actor we want to animate:
Code: ( (Unknown Language)) [Select]
self.cas = 0;
Now, we'll be creating a Begin Step event.
We have to decide how to handle the possibility that the actor might be unable to move through the path we say.
There are two possibilities: ignore that move, or wait until the move it valid. Both are very easy to implement.

Let's say we want to ignore the invalid moves:
Code: ( (Unknown Language)) [Select]
if (!moving) {    if (self.cas == 0 || self.cas == 1) {        self.orientation = 0;        perform_motion(); self.cas += 1;    }    if (self.cas == 2) {        self.orientation = 1;        perform_motion(); self.cas += 1;    }}
If, instead, we want to wait until the move it valid, we simply do:
Code: ( (Unknown Language)) [Select]
if (!moving) {    if (self.cas == 0 || self.cas == 1) {        self.orientation = 0;        perform_motion();    }    if (self.cas == 2) {        self.orientation = 1;        perform_motion();    }    if (moving) self.cas += 1;}
We can even show messages after moving, etc.
Do note that the player is still able to move during these events, etc.

Future Work

Right now, the engine is already very powerful yet incredibly simple.
There are, however, numerous possible improvements that would easily fit this design:

- Create a "Finished Moving" user event. This would happen every time a game step happened(not the GM definition of step, but instead actually moving a tile). This is essential for random encounters and step counters.
- Add support for views.
- Right now, events occur in parallel with the rest of the game. The hero is still able to move, etc. It might be important for cutscenes, etc.
- Improve integration of action sequences with the rest of the engine. Right now, this can be implemented with extra variables and special handling, but it's too disorganized. Instead, the action sequence mechanism could rely on an user event.
- Right now, pressing the Space button always closes the messages. It might be worth making this customizable(to instead allow waiting X time before closing, etc.)

The End

Although this tutorial might be pretty big, the engine itself is very small and simple.
The version of ENIGMA I tested against doesn't seem to handle it, but mine is not up-to-date so newer versions *might* work. Worst-case, Josh will have a new unit test to play with.
This engine was designed to be easily extensible and simple to understand.

I hope that at least one person in the world will be able to read and understand this entire article...

That's all for now.
« Last Edit: February 20, 2011, 07:45:15 PM by luiscubal » Logged
Offline (Male) Rusky
Reply #4 Posted on: February 20, 2011, 07:27:23 PM

Resident Troll
Joined: Feb 2008
Posts: 960
MSN Messenger - rpjohnst@gmail.com
View Profile WWW Email
This looks pretty solid; I like the actor/window system and the fact that you can focus on different pieces of the game, although I'm not sure I like the implementation (it could be vastly improved rather easily in something not GM). Frame rate independence would be a nice addition, and shouldn't be too hard considering you already have room speed independence.

However, you have a lot of verbosity that obscures meaning more than keeping DRY. For instance, you could use object_is_ancestor rather than a chain of three scripts and a for loop to manually follow the inheritance chain. The use of self is also rather useless, although it may be useful in a few situations to disambiguate names.

The long chains of if statements on orientation are both verbose and inefficient. It would be much better to do some variant of
Code: [Select]
x += right - left
y += down - up
where right/left/down/up are expressions based on keyboard input or some appropriate abstraction. It takes a little bit more work to tell an actor's orientation, but it's well worth it.
Logged
Offline (Unknown gender) luiscubal
Reply #5 Posted on: February 20, 2011, 07:42:35 PM
Member
Joined: Jun 2009
Posts: 452

View Profile Email
@Rusky I was not aware of the existence of the object_is_ancestor function. I will edit the article to use this function instead.

I'm not sure what you mean with the orientation. Are you saying to that I should detect which keys are pressed and then use that instead of orientation?
If that's what you mean, I dislike that approach because I'd like the orientation system to be well isolated from the keyboard input system.
I tried to address the orientation verbosity problem by isolating the millions of if statements in functions like x_in_direction/y_in_direction, which I hope will reduce the number of actual if statements in the actual game.
Logged
Offline (Male) Rusky
Reply #6 Posted on: February 21, 2011, 02:00:08 PM

Resident Troll
Joined: Feb 2008
Posts: 960
MSN Messenger - rpjohnst@gmail.com
View Profile WWW Email
You could base it on the keyboard "or some appropriate abstraction." All I'm saying is I don't like orientation as the internal representation of the character. In a tile-based game there's no need to keep it as a first-class citizen- when you do need it it's easier to just recalculate it from the much more useful information of current and previous tiles.
Logged
Offline (Unknown gender) luiscubal
Reply #7 Posted on: February 21, 2011, 02:49:01 PM
Member
Joined: Jun 2009
Posts: 452

View Profile Email
@Rusky And how do you propose to do it in GM?
I guess it's fairly easy to handle with virtual methods, but how do you propose to do that in GM?
Also, self.orientation is simple to understand and manipulate. It actually seems to scale pretty well for what I want the engine to be.
Finally, orientation has meaning even when no keys are being pressed. For instance, to ensure the main hero instance keeps looking towards the same direction even after it stops moving.

In what situations would not having this variable (and instead recalculate it based on some method) be more useful, simpler or easier to understand/maintain?
Logged
Offline (Male) Rusky
Reply #8 Posted on: February 21, 2011, 05:29:03 PM

Resident Troll
Joined: Feb 2008
Posts: 960
MSN Messenger - rpjohnst@gmail.com
View Profile WWW Email
You can have the previous tile mean something when you haven't moved. Just don't update it and it will the last different tile. That way you have the information in a format that makes sense with tiles- you can easily get the direction to push a block, for example, with (x - xp, y - yp). When you use orientation directly you have to do a lot more management (even if it's all contained in a few death-scripts) to make sure everything's consistent.
Logged
Offline (Unknown gender) luiscubal
Reply #9 Posted on: February 21, 2011, 05:42:20 PM
Member
Joined: Jun 2009
Posts: 452

View Profile Email
Sometimes, the previous tile means nothing. Consider a NPC that is always in the same place. When the hero approaches that NPC and talk to it, it looks at the hero and keeps looking that way. Then I approach in a different location and the same repeats.
The previous tile is only meaningful as an orientation metric for moving actors.
Logged
Offline (Male) Rusky
Reply #10 Posted on: February 21, 2011, 09:05:52 PM

Resident Troll
Joined: Feb 2008
Posts: 960
MSN Messenger - rpjohnst@gmail.com
View Profile WWW Email
That's true, but couldn't you solve that by a different implementation of the same interface for moving and non-moving actors, rather than forcing moving actors to deal with long if-chains just to get the data they should be storing anyway?
Logged
Offline (Unknown gender) luiscubal
Reply #11 Posted on: February 22, 2011, 06:15:02 AM
Member
Joined: Jun 2009
Posts: 452

View Profile Email
I *could*, but this system works and is very simple.
I could have movable_obj/fixed_obj extend actor_obj, but sometimes actors are mixed - they ocasionally move and ocasionally stay in the same place. And I'd rather not change the object type of instances at runtime.

If accessing the previous tile is really important for RPGs, I suppose I could implement that as a separate variable, independent from orientation.
Logged
Post made February 22, 2011, 11:38:25 AM was deleted at the author's request.
Pages: [1]
  Print