Game Builder

Game Builder

Not enough ratings
How to animate the terrain
By Sandles
Are you struggling to create more dynamic games? Is the world too static for you, are you looking for more dynamic worlds to build with Game Builder? Look no further, because the Terrain Animator is here!

Are you interested in secret caves, self-building towns, ocean waves, meteors, trap rooms? The possibilities are endless with the Terrain Animator - let me show you how it works.
   
Award
Favorite
Favorited
Unfavorite
What we will be building
Prerequisites
In order for you to use the Terrain Animator, you must first be using the helper functions I wrote to create/clear blocks relative to a location. You won't be able to use the Terrain Animator without them. These helper functions are in the code below, so as long as you are using the template below, you will have the necessary code to use the Terrain Animator.

If you want to understand more about these helper functions, you may visit the link above, but it is not necessary to fully understand them to use the Terrain Animator.
The code
In order for you to use the Terrain Animator, you will need to use the following code, you will put this code in your card's Code tab.


// Example card.

// User-editable properties for this card:
export const PROPS = [
propNumber("ticks", 60),
propBoolean("infinite", true),
propNumber("forceFrame", -1)
];

/**
* Sets the block relative to the pos.
* @param {THREE.Vector3} pos The relative position (typically an actor)
* @param {int} xOffset The x-offset relative to pos where you'd like to place the block
* @param {int} yOffset The y-offset relative to pos where you'd like to place the block
* @param {int} zOffset The z-offset relative to pos where you'd like to place the block
* @param {BlockShape} blockShape The shape of the block
* @param {BlockDir} blockDir The direction of the block
* @param {BlockStyle} blockStyle The style of the block
*/
var sbr = function (pos, xOffset, yOffset, zOffset,
blockShape, blockDir, blockStyle) {
// Blocks extend 2.5 units on the x-plane,
// and 1.5 units in the y-plane
var newX = (pos.x + (xOffset * 2.5)) / 2.5;
var newY = (pos.y + (yOffset * 1.5)) / 1.5;
var newZ = (pos.z + (zOffset * 2.5)) / 2.5;

setBlock(
newX,
newY,
newZ,
blockShape,
blockDir,
blockStyle
);
};

/**
* Clears the block relative to the pos.
* @param {THREE.Vector3} pos The relative position (typically an actor)
* @param {int} xOffset The x-offset relative to pos where you'd like to clear the block
* @param {int} yOffset The y-offset relative to pos where you'd like to clear the block
* @param {int} zOffset The z-offset relative to pos where you'd like to clear the block
*/
var cbr = function (pos, xOffset, yOffset, zOffset) {
// Blocks extend 2.5 units on the x-plane,
// and 1.5 units in the y-plane
var newX = (pos.x + (xOffset * 2.5)) / 2.5;
var newY = (pos.y + (yOffset * 1.5)) / 1.5;
var newZ = (pos.z + (zOffset * 2.5)) / 2.5;

clearBlock(
newX,
newY,
newZ
);
};

var Animation = (function (origin) {
var _tick = 0; // Holds the count of ticks
var _frames = []; // All of our frames for our animation
var _currentFrame = 0; // The current frame we are showing
var _drawnFirstFrame = false; // If we've drawn the first frame yet
var _ended = false; // If the animation should be ended
var _origin = origin; // Holds the origin (position (x,y,z)) of this animation
var _savedBlocks = {}; // Holds saved blocks used in your animation - to save typing!

/**
* Clears out all frames.
*/
var clearAllFrames = function () {
for (var i = 0; i < _frames.length; i++) {
for (var j = 0; j < _frames[i].length; j++) {
cbr(_origin, _frames[i][j].x, _frames[i][j].y, _frames[i][j].z);
}
}
};

/**
* The main drawing function.
* @param {int} frameNum The frame to display
*/
var draw = function (frameNum) {
clearAllFrames();

// Draw new blocks
for (var i = 0; i < _frames[frameNum].length; i++) {

// Load from saved blocks
if (typeof _frames[frameNum][i].block !== "undefined") {
var block = _savedBlocks[_frames[frameNum][i].block];

sbr(_origin, _frames[frameNum][i].x, _frames[frameNum][i].y, _frames[frameNum][i].z,
block.shape, block.dir, block.style);
} else {
sbr(_origin, _frames[frameNum][i].x, _frames[frameNum][i].y, _frames[frameNum][i].z,
_frames[frameNum][i].shape, _frames[frameNum][i].dir, _frames[frameNum][i].style);
}
}
};

return {
/**
* Sets the origin (x,y,z) of where this animation should originate from.
* @param {THREE.Vector3} origin
*/
setOrigin: function (origin) {
_origin = origin;
},
/**
* Saves block data so that you can reference the data by a name,
* instead of typing out the block data for each identical block.
* @param {Object} block Block information that should be saved
* @param {string} name A friendly name of this block
*/
saveBlock: function (block, name) {
if (typeof _savedBlocks[name] === "undefined") {
_savedBlocks[name] = block;
}
},
/**
* To be called from the "onTick" function.
*/
play: function () {
// If the animation is one-time,
// don't attempt to run it again
if (_ended && !props.infinite) {
return;
}

// Force a particular frame, only if it's in the
// range of available frames.
if (props.forceFrame >= 0 && props.forceFrame < _frames.length) {
_currentFrame = props.forceFrame;

draw(props.forceFrame);
return;
}

_tick++;
if (_tick >= props.ticks) {
_tick = 0;
_currentFrame++;

if (_currentFrame >= _frames.length) {
if (!props.infinite) {
_ended = true;
clearAllFrames();
return;
} else {
_currentFrame = 0;
}
}
draw(_currentFrame);
} else {

// Draw the first frame (only once)
if (!_drawnFirstFrame && _currentFrame === 0) {
_drawnFirstFrame = true;
draw(_currentFrame);
}
}
},
/**
* Adds a block, or an array of blocks to be saved
* for a single frame.
* @param {array} blocks An array of blocks for a given frame
*/
addFrame: function (blocks) {
if (!Array.isArray(blocks)) {
_frames.push([blocks]);
} else {
_frames.push(blocks);
}
}
}
})();

// Save blocks that we want to use later
// ...

// Save animation frame data
// ...

// onTick is called every frame (50-60 times per second).
export function onTick() {
Animation.setOrigin(getPos());
Animation.play();
}

export function onCollision(msg) {
cooldown(1);
}

What does this code mean/do?
This code creates an object that stores animation frames, and plays the animation back based on the frame data you pass in. The above example does not has any frame data in it, we will discuss how to do that in the next section.
Adding animation frame data
In order to animate terrain, you need to define what each frame is supposed to look like. Think of a frame like a flip book, each page has an image. By themselves, each image is static, but when flipping the book (changing frames), the image seems to "come alive."

We will be doing the same thing with the terrain in Game Builder. You notice in the code above, there is a section like this.

// Save animation frame data // ...

It is in this section that we will add frame data for our animation. The syntax (code) you will need to add looks like this. The necessary data is the (x,y,z) for the block, and the block's shape, direction and style.

Animation.addFrame([{ x: 0, y: 0, z: 0, shape: BlockShape.BOX, dir: BlockDir.NORTH, style: BlockStyle.COLOR1 }]);

Each call to "Animation.addFrame" will be another frame in your animation.

Adding multiple blocks per frame
Now, it is very likely that you will have more than a single block that is a part of a frame. In order to add multiple blocks to a frame, just separate the object { } with a comma. See the below snippet for an example.

Animation.addFrame([{ x: 0, y: 0, z: 0, shape: BlockShape.BOX, dir: BlockDir.NORTH, style: BlockStyle.COLOR1 }, { x: 0, y: 1, z: 0, shape: BlockShape.BOX, dir: BlockDir.NORTH, style: BlockStyle.COLOR1 }]);

Important note!
Like in the previous guide, the (x,y,z) values you pass into this code above is the (x,y,z) offset from the origin of the animation. We will go over where the origin of the Terrain Animator is set next.
Starting the animation
Starting the Terrain Animator is easy! Simply stick the following code in the "onTick" function.

// onTick is called every frame (50-60 times per second). export function onTick() { Animation.setOrigin(getPos()); Animation.play(); }

The Terrain Animator will use the position of the actor you've attached this Terrain Animator (card) to be the origin of the animation.
Adjusting options
The Terrain Animator has 3 options you can use.



ticks
Ticks is a value that determines how fast your animation will play. Game Builder will tick between 50-60 times every second, so if you want each frame to take roughly a second, set ticks to 60.

infinite
If you want your animation to replay infinitely, check this option. If you want your animation to run only once, uncheck this option.

forceFrame
In order to test your animation, you can change this value and view what each frame will look like. The first frame is 0! To stop testing a particular frame, set this back to -1.
Saving commonly-used blocks in your animation
I found it's a lot of typing to type out the shape, direction and style of every single block in an animation, so I made an easy workaround in order to save your fingers from typing so much. For any block that you will be using a lot in your animation, you can save it to your animation and reference it by a name in your frame data.

So, as an example, let's look at our previous example of adding a single block to a frame.

Animation.addFrame([{ x: 0, y: 0, z: 0, shape: BlockShape.BOX, dir: BlockDir.NORTH, style: BlockStyle.COLOR1 }]);

Instead of typing out all this information about the block, let's save this block with a friendly name so we can use it later. It is recommended you stick this code in the following section of the template above.

// Save blocks that we want to use later Animation.saveBlock({ shape: BlockShape.BOX, dir: BlockDir.NORTH, style: BlockStyle.COLOR1 }, "gray");

Now, once we've saved our "gray" block, we can use it in our animation frame data.

Animation.addFrame([{ x: 0, y: 0, z: 0, block: "gray" }]);
An example; a wave
This is something I wanted to build (a wave), so I made the Terrain Animator for this purpose. Below you will see how this looks and the following code that makes it happen.

*Please ignore the texture on the block, there still are things the Game Builder team is working through fixing.


Full code

// Example card.

// User-editable properties for this card:
export const PROPS = [
propNumber("ticks", 60),
propBoolean("infinite", true),
propNumber("forceFrame", -1)
];

/**
* Sets the block relative to the pos.
* @param {THREE.Vector3} pos The relative position (typically an actor)
* @param {int} xOffset The x-offset relative to pos where you'd like to place the block
* @param {int} yOffset The y-offset relative to pos where you'd like to place the block
* @param {int} zOffset The z-offset relative to pos where you'd like to place the block
* @param {BlockShape} blockShape The shape of the block
* @param {BlockDir} blockDir The direction of the block
* @param {BlockStyle} blockStyle The style of the block
*/
var sbr = function (pos, xOffset, yOffset, zOffset,
blockShape, blockDir, blockStyle) {
// Blocks extend 2.5 units on the x-plane,
// and 1.5 units in the y-plane
var newX = (pos.x + (xOffset * 2.5)) / 2.5;
var newY = (pos.y + (yOffset * 1.5)) / 1.5;
var newZ = (pos.z + (zOffset * 2.5)) / 2.5;

setBlock(
newX,
newY,
newZ,
blockShape,
blockDir,
blockStyle
);
};

/**
* Clears the block relative to the pos.
* @param {THREE.Vector3} pos The relative position (typically an actor)
* @param {int} xOffset The x-offset relative to pos where you'd like to clear the block
* @param {int} yOffset The y-offset relative to pos where you'd like to clear the block
* @param {int} zOffset The z-offset relative to pos where you'd like to clear the block
*/
var cbr = function (pos, xOffset, yOffset, zOffset) {
// Blocks extend 2.5 units on the x-plane,
// and 1.5 units in the y-plane
var newX = (pos.x + (xOffset * 2.5)) / 2.5;
var newY = (pos.y + (yOffset * 1.5)) / 1.5;
var newZ = (pos.z + (zOffset * 2.5)) / 2.5;

clearBlock(
newX,
newY,
newZ
);
};

var Animation = (function (origin) {
var _tick = 0; // Holds the count of ticks
var _frames = []; // All of our frames for our animation
var _currentFrame = 0; // The current frame we are showing
var _drawnFirstFrame = false; // If we've drawn the first frame yet
var _ended = false; // If the animation should be ended
var _origin = origin; // Holds the origin (position (x,y,z)) of this animation
var _savedBlocks = {}; // Holds saved blocks used in your animation - to save typing!

/**
* Clears out all frames.
*/
var clearAllFrames = function () {
for (var i = 0; i < _frames.length; i++) {
for (var j = 0; j < _frames[i].length; j++) {
cbr(_origin, _frames[i][j].x, _frames[i][j].y, _frames[i][j].z);
}
}
};

/**
* The main drawing function.
* @param {int} frameNum The frame to display
*/
var draw = function (frameNum) {
clearAllFrames();

// Draw new blocks
for (var i = 0; i < _frames[frameNum].length; i++) {

// Load from saved blocks
if (typeof _frames[frameNum][i].block !== "undefined") {
var block = _savedBlocks[_frames[frameNum][i].block];

sbr(_origin, _frames[frameNum][i].x, _frames[frameNum][i].y, _frames[frameNum][i].z,
block.shape, block.dir, block.style);
} else {
sbr(_origin, _frames[frameNum][i].x, _frames[frameNum][i].y, _frames[frameNum][i].z,
_frames[frameNum][i].shape, _frames[frameNum][i].dir, _frames[frameNum][i].style);
}
}
};

return {
/**
* Sets the origin (x,y,z) of where this animation should originate from.
* @param {THREE.Vector3} origin
*/
setOrigin: function (origin) {
_origin = origin;
},
/**
* Saves block data so that you can reference the data by a name,
* instead of typing out the block data for each identical block.
* @param {Object} block Block information that should be saved
* @param {string} name A friendly name of this block
*/
saveBlock: function (block, name) {
if (typeof _savedBlocks[name] === "undefined") {
_savedBlocks[name] = block;
}
},
/**
* To be called from the "onTick" function.
*/
play: function () {
// If the animation is one-time,
// don't attempt to run it again
if (_ended && !props.infinite) {
return;
}

// Force a particular frame, only if it's in the
// range of available frames.
if (props.forceFrame >= 0 && props.forceFrame < _frames.length) {
_currentFrame = props.forceFrame;

draw(props.forceFrame);
return;
}

_tick++;
if (_tick >= props.ticks) {
_tick = 0;
_currentFrame++;

if (_currentFrame >= _frames.length) {
if (!props.infinite) {
_ended = true;
clearAllFrames();
return;
} else {
_currentFrame = 0;
}
}
draw(_currentFrame);
} else {

// Draw the first frame (only once)
if (!_drawnFirstFrame && _currentFrame === 0) {
_drawnFirstFrame = true;
draw(_currentFrame);
}
}
},
/**
* Adds a block, or an array of blocks to be saved
* for a single frame.
* @param {array} blocks An array of blocks for a given frame
*/
addFrame: function (blocks) {
if (!Array.isArray(blocks)) {
_frames.push([blocks]);
} else {
_frames.push(blocks);
}
}
}
})();

// Save blocks that we want to use later
Animation.saveBlock({
shape: BlockShape.BOX,
dir: BlockDir.NORTH,
style: 26 // ocean, doesn't work atm
}, "ocean");

// Save animation frame data
Animation.addFrame([{
x: 0,
y: 0,
z: 6,
shape: BlockShape.BOX,
dir: BlockDir.NORTH,
style: 26
}]);
Animation.addFrame([{
x: 0,
y: 0,
z: 5,
block: "ocean"
}, {
x: 0,
y: 0,
z: 4,
block: "ocean"
}]);
Animation.addFrame([{
x: 0,
y: 0,
z: 4,
block: "ocean"
}, {
x: 0,
y: 0,
z: 3,
block: "ocean"
}, {
x: -1,
y: 0,
z: 3,
block: "ocean"
}, {
x: 1,
y: 0,
z: 3,
block: "ocean"
}, {
x: 0,
y: 1,
z: 3,
block: "ocean"
}, {
x: 0,
y: 0,
z: 2,
block: "ocean"
}]);
Animation.addFrame([{
x: 0,
y: 0,
z: 2,
block: "ocean"
}, {
x: 0,
y: 0,
z: 1,
block: "ocean"
}]);
Animation.addFrame([{
x: 0,
y: 0,
z: 0,
block: "ocean"
}]);

// onTick is called every frame (50-60 times per second).
export function onTick() {
Animation.setOrigin(getPos());
Animation.play();
}

export function onCollision(msg) {
cooldown(1);
}
Thanks for reading!
Thanks for reading about the Terrain Animator, I hope that you will find it useful in your projects.
2 Comments
Sandles  [author] 19 Jun, 2019 @ 4:23am 
I'm glad it is helpful, Patrick. Great suggestion, I'll do that in a little bit.
gg 19 Jun, 2019 @ 1:45am 
This is incredibly useful! Appreciate your efforts! It would be perfect to show the result at the top so that beginners might have an abstract concept about what they going to build when following your guide.