Kasper Andersson Brandt


Game Programmer

GitHub LinkedIn CV


Bale Jumper


Endless runner
Easter egg for Presona
Siemens TIA Portal | JavaScript



I was brought on to help modernize the interface for the touch panels on Presona's balers, and as a bonus I made this little Easter egg. It was an interesting challenge to make a game using something not at all intended to make games. My main concern was making sure I could make an update loop with delta time, and once I had figured out how to do that the rest was pretty straightforward.

The programs you make for Siemens touch panels are made of screens containing items on different layers. In this game I used several items of the types image, text, and button. For example: the player has two images which are alternately shown and hidden in order to look like running, and one image which is moved up and then down to look like jumping. You can define callback functions on the items, which differ depending on the type of item. In the game I defined three such functions as well as a global (for the screen) update function. Firstly the root item (the screen) has a function for when it is loaded, where I define a bunch of variables (get references to items) and handle the intro of a baler sliding to the right. The baler movement uses very similar code to the update function.


export function _9_Game_OnLoaded(item) {
	start = Screen.FindItem("btnStart");
	title = Screen.FindItem("txtTitle");
	over = Screen.FindItem("txtGameOver");
	scoreText = Screen.FindItem("txtScore");
	playerJump = Screen.FindItem("playerJump");
	playerRun0 = Screen.FindItem("playerRun0");
	playerRun1 = Screen.FindItem("playerRun1");
	playerLeft = playerJump.Left + 19;
	playerRight = playerJump.Left + playerJump.Width - 16;
	playerHeight = playerJump.Height - 13;
	playerTop = playerJump.Top;
	for (let i = 0; i < 8; i++) {
		bales.push(Screen.FindItem("bale" + i));
	}
	baleTop = bales[0].Top;
	baleStart = item.Width;

	baler = Screen.FindItem("imgBaler");
	balerEnd = baler.Left;
	baler.Left = balerStart;
	prevTime = Date.now();
	updateID = HMIRuntime.Timers.SetInterval(function() {
		let time = Date.now();
		let delta = (time - prevTime) / 1000;
		prevTime = time;
		baler.Left += baleSpeed * delta;
		if (baler.Left >= balerEnd) {
			baler.Left = balerEnd;
			start.Visible = true;
			title.Visible = true;
			scoreText.Visible = true;
			Screen.FindItem("txtScoreLabel").Visible = true;
			playerRun0.Visible = true;
			Screen.FindItem("imgJump").Visible = true;
			Screen.FindItem("btnJump").Visible = true;
			for (let i = 0; i < 17; i++) {
				Screen.FindItem("imgConveyor" + i).Visible = true;
			}
			HMIRuntime.Timers.ClearInterval(updateID);
		}
	}, 20);
}

When the baler has finished moving, the rest of the game elements become visible. The next function to be called is the callback for tapping the start button. This function hides the title, game over text, and start button, as well as resetting game elements and starting the update loop. In order to have an update loop I used HMIRuntime.Timers.SetInterval to call the update function every 20 milliseconds (50 frames per second), and in order to calculate the delta time I used Date.now (as it won't be exactly 20 milliseconds between updates).


export function btnStart_OnTapped(item, x, y, modifiers, trigger) {
	start.Visible = false;
	title.Visible = false;
	over.Visible = false;
	scoreCountdown = scoreTime;
	score = 0;
	scoreText.Text = score;
	playerJump.Top = playerTop;
	playerJump.Visible = false;
	playerRun0.Visible = true;
	playerRun1.Visible = false;
	runCountdown = runTime;
	jumping = false;
	baleCountdown = 0;
	activeBales = [];
	inactiveBales = [];
	for (let i = 0; i < bales.length; i++) {
		inactiveBales.push(i);
		bales[i].Left = baleStart;
	}
	prevTime = Date.now();
	updateID = HMIRuntime.Timers.SetInterval(update, 20);
}

When the game is running (determined by checking if the start button is hidden), you can tap the jump button. This shows the player's jumping image, hides the two running images, and sets the values for a jump.


export function btnJump_OnDown(item, x, y, modifiers, trigger) {
	if (jumping || start.Visible) {
		return;
	}
	playerJump.Visible = true;
	playerRun0.Visible = false;
	playerRun1.Visible = false;
	jumpSpeed = startSpeed;
	jumping = true;
}

The global definition area is where all global values are declared, and where the update function is defined. All of the constant values were simply defined by what felt/looked good.

The update function starts with calculating the delta time, and then counting down to a new (randomly selected) bale spawning. References to the bales are stored in two arrays in order to keep track of which ones are currently active and which ones are available to be (re)spawned. Then the active bales are moved to the left and checked for collision with the player. If a collision is found the update loop is stopped, and the title, game over text, and start button are shown. Otherwise the function continues with moving the player up and down if jumping, or swapping between the running images if not. Lastly the score is updated with one point each second.


var start;
var title;
var over;

var updateID;
var prevTime;

var scoreCountdown;
const scoreTime = 1;
var score;
var scoreText;

var playerJump;
var playerRun0;
var playerRun1;
var playerLeft;
var playerRight;
var playerHeight;
var playerTop;
var runCountdown;
const runTime = 0.2;
var jumping;
var jumpSpeed;
const startSpeed = 320;
const gravity = 400;

var bales = [];
var activeBales;
var inactiveBales;
var baleCountdown;
const baleMinTime = 1.6;
var baleTop;
var baleStart;
const baleSpeed = 200;

var baler;
const balerStart = 65;
var balerEnd;

function update() {
	let time = Date.now();
	let delta = (time - prevTime) / 1000;
	prevTime = time;

	baleCountdown -= delta;
	if (baleCountdown <= 0 && inactiveBales.length > 0) {
		baleCountdown = baleMinTime + Math.random();
		let i = Math.floor(Math.random() * inactiveBales.length);
		activeBales.push(inactiveBales[i]);
		inactiveBales.splice(i, 1);
	}

	let playerBottom = playerJump.Top + playerHeight;
	for (let i = 0; i < activeBales.length; i++) {
		let bale = bales[activeBales[i]];
		bale.Left -= baleSpeed * delta;
		if (playerBottom > baleTop && playerRight > bale.Left && playerLeft < bale.Left + bale.Width) {
			start.Visible = true;
			title.Visible = true;
			over.Visible = true;
			HMIRuntime.Timers.ClearInterval(updateID);
			return;
		}
		if (bale.Left < 0 - bale.Width) {
			bale.Left = baleStart;
			inactiveBales.push(activeBales[i]);
			activeBales.splice(i, 1);
			i--;
		}
	}

	if (jumping) {
		playerJump.Top -= jumpSpeed * delta;
		jumpSpeed -= gravity * delta;
		if (playerJump.Top >= playerTop) {
			playerJump.Top = playerTop;
			playerJump.Visible = false;
			playerRun0.Visible = true;
			runCountdown = runTime;
			jumping = false;
		}
	} else {
		runCountdown -= delta;
		if (runCountdown <= 0) {
			runCountdown += runTime;
			if (playerRun0.Visible) {
				playerRun0.Visible = false;
				playerRun1.Visible = true;
			} else {
				playerRun0.Visible = true;
				playerRun1.Visible = false;
			}
		}
	}

	scoreCountdown -= delta;
	if (scoreCountdown <= 0) {
		scoreCountdown += scoreTime;
		score++;
		scoreText.Text = score;
	}
}