A System for Scripting Moving Obstacles in Vacuous
I have decided to give a little more insight into the systems inside Vacuous. In particular, I want to discuss the system I created for scripting all of the moving bits and pieces in each level of the game. Vacuous, much like platformer games, is often about avoiding obstacles. Since the ship the player controls is actually a very delicate bomb, it explodes whenever it comes into contact with a solid object. Giving these solid objects motion makes them a more interesting "opponent." Thus, a system for creating and controlling these obstacle's motion was an essential element for the game to work. In fact, all of this was created very early in development. This system works directly in Game Maker Studio's level editor.
It all begins with this game object: the abstract moving wall. Any other world object in the game that is designed for movement inherits the behavior of this abstract object. This creates an incredibly simple pipeline for adding new moving obstacles into the game. The process is as follows: create a new object, make sure it is a child of the abstract moving wall object, and assign it a sprite and collision mask.
But what exactly is the behavior of the abstract moving wall object that allows its children to have scriptable motion? The abstract moving wall has a create and step event; in the create event the values used for motion are declared and given default values. In the step event, the motion of the object is calculated and then applied.
Here is the second part of the equation: the moving node object. These nodes are used as control points that guide the motion of moving objects. They have two events: create and room start. When the node is created, the values it uses for guiding moving walls are declared and given default values. In the room start event, the node is added to a list of a nodes (Game Maker's list data structure). This was done for quick access of the nodes. However, it might not actually be faster. It is not clear to me how Game Maker handles the data structure that contains all the objects in a room. It might be well designed enough that this isn't neccesary and a call to instance_find() in a for loop of instance_number() length is sufficient. This code was written before I knew of this method and I am not in any rush to refactor it since the worst it can do is take up tiny bit of extra memory in each stage.
So what variables are defined when a moving object is created and what do they do? Here is the script that answers that question. The convention here is that any variable with a //!!! comment after it is meant to be modified. Obviously a more robust protection system should be used, but Game Maker lacks one. Thankfully, there is only one developer. The comments on the script do a good job themselves describing the function of each variable. We have the destination node the object is currently translating towards, the speed of that translation, the speed and direction of the object's rotation, and the speed the object grows or shrinks. These define the three basic types of transformation an object can have: translation, rotation, and scaling. Any combination of these can be used at a given time.
There is then a value that stores the actual id of the node, not its name. Node names are added as an alias to the id to make scripting much cleaner; defining a translation to a named node is more readable than defining one towards an id number consisting of random characters. Angular translation is a variation of translation that instead of having the object move from node to node has it rotating around a node. All the object needs to know is that it has angular translation and what node it is rotating around. From there, other values like radius and angle to the node can be derived for use in computation. There is also the option to make the object pause its motion when it reaches a node before beginning its new motion instructions. The extra distance value is used behind the scenes to deal with fast moving objects (to be explained). Finally, a sound can be played when an object reaches its destination.
It can be seen that the creation script defining all of the variables for the node is incredibly similar to the creation script for the moving object. The only difference is the variable naming the node. This is because each node contains a set of movement instructions for the moving object. If the moving object reaches that node, it picks up that node's movement properties. This system is powerful. The moving object can be initially defined to go to a node on the left of the stage. Once it reaches that node, the object gets new instructions to move to a node on the right of the stage. Then the node gives instructions to the moving object to move to the node on the left and the cycle continues forever. This allows for a cycle of motion for moving objects to be defined consisting of translation, rotation, scaling, and angular translation allowing for many unique level designs to be implemented.
Let us take a closer look at the script which is run on the step event of every moving object. This is the meat of the whole entire system. It starts by making sure that the instance id of the destination node is stored for use in the moving object. If it isn't, all the node ids are looped through until the node with the given name is found. This happens each time a moving object gets new movement instructions; the instructions only give the name of the destination node and not the id, so the id needs to be found.
The is the very same process as above, but this is the case where the object is planning to do angular translation and needs the angular source node id. This is used to make objects orbit in a circular path around a position in the room.
Here is the code that rotates the moving object. I got pretty lucky here. Rotating the image angle in Game Maker not only rotates the appearance of an object but also rotates its collision mask. This is incredibly helpful, since it means all I need to do to rotate the object is either increase or decrease the image_angle value. Combining rotation with translation can create exciting obstacles for the player.
With this code I handle object translation. If the moving game object has a destination, each update it moves towards that destination at its given velocity. For very fast moving objects, there is the potential to overshoot the destination in a single update. The code checks after stepping if the object has overshot the destination and adjusts it accordingly.
Once the object has reached the destination node, it needs to get its next orders. Again, there is some overshooting compensation here. If the object is moving really fast and in one step is already way past the destination, it is set on the route to the next one right away. This doesn't account for objects that are going to overshoot by two or more nodes, but it then becomes the designer's responsibility to implement objects that don't move that fast.
This is an example of how moving objects are set up in the level editor. In particular, these is an angular translation scenario. The two ice trianlges rotate around the central node. The node is given a name and each triangle is pointed to it and given a speed to orbit at. All done with the vanilla Game Maker Studio room edtior! Though with a custom tool these tasks could be even further streamlined.
- J. M. Stark