In this tutorial, we will be making an egg catching game. This is based on a real game that is also published on the actual mobile app stores.

February 2016

Check out the following links to get an idea of the kind of game we will be making. Once you are done with this tutorial, you can have made a game similar to the one available through the links below:

This tutorial is developed using the EQ Programming Language and the Jkop libraries.

To start with our game, we only need to have the tool and ourselves ready.

The tool: Eqela Code Studio

This tutorial assumes the use of the latest Eqela Code Studio:

Download and install Eqela Code Studio

Start working on Eqela Code Studio

Open Eqela Code Studio, then click on "New".

A dialog box will appear with various project types, but for the purpose of this tutorial we will select "Blank Eqela Project". Then click on "Next".

Now fill up the "Project Details":

(1) Give a descriptive name for the project in the Project name field

(2) Give a Vendor ID or the module name (What is a Vendor ID?). This is expected in reverse-domain notation. On the filesystem, a module is the same thing as a directory; the name of the module is the name of the directory.

(3) Destination Directory is where the project files will be created; the default value is the "projects" directory inside EqelaWorkspaceStudio (on Windows, this is found under the Documents directory; on Mac and Linux it is found directly under the home directory)

(4) Click "Done"

A new window will be opened, initially containing an "eqela.eqproject" file with the details you specified (as much as possible, do not edit this file as it is used for compilation of the project).

The newly created project also includes a file named "eqela.config" where project settings are defined, and Main.eq that serves as the entry point of the project (EQ programming language source code files end with a ".eq" extension).

Work on the configuration file of the project

We need to configure the project settings contained in the eqela.config file: primarily, this means the module type and the dependencies (required library modules) for the project.

Click "Open", then select/open the module name of the project and select the "eqela.config" file. The "displayname" field defines the user-visible name for the application, while "moduletype" defines the type of the current module. For the purposes of this tutorial, we will be using "customguiapplication".

Module or library dependencies are configured using the "depends" keyword. For the purposes of this tutorial, we will use three libraries, which will be declared in three separate depends statements:

depends: jkop.sprite
depends: jkop.sprite.engine
depends: jkop.sprite.util

Read more about Jkop Sprite

You have got a friend

See the Eqela API reference for more details about all of the classes and methods that are available. Soon, we will use them.

You can also download the source code of Jkop for additional reference.

When you are all set and ready to go, follow the tutorial steps below in the given sequence:

Description of the game

We will create a catching game, where the player, using a basket, needs to catch the fresh eggs. On the upper left side of the screen, we will place the current score, current high score, game time, number of ordered eggs, and the remaining number of eggs. On the upper right side of the screen, we will place the "quit" button that will take the player back to the menu scene.

There will be a chicken that produces the eggs, and it will be on the top portion of the screen, just below the labels, and it will move from left to right and back again.

On the lower portion of the screen, we will place the basket that will catch the eggs. The basket moves through pointer (whether mouse or touch) click and drag, and it will also move from left to right and back again, the same as the chicken.

There will be a 60 second time allotment for the player. The goal of the player is to deliver as many orders of eggs as possible within the given time.

We will also place a delivery truck on the mid-left part of the screen. The transparency of the truck is initially at 50%, and it will turn to its full color when the required number of eggs to be delivered has been reached. The player must deliver the eggs by tapping the truck. If the required number of eggs is reached, it must be delivered so that the player can get another order, or else the basket cannot catch eggs anymore.

To add more fun to the game, the chicken will produce two types of eggs: The other will be a fresh egg which will be needed by the player, and the other will be a rotten egg that should be avoided by the player.

The player must catch all the fresh eggs. The player is only allowed to miss 5 fresh eggs. If ever the player accidentally catch a rotten egg, the total number of caught eggs will be decreased.

For the image resources, get it here.

Working on classes

The main class

Open the "Main.eq" file. The Main class serves to be the entry point of the project, and will extend SEApplication, denoted by a colon after the class name. We will simply return the first scene in the application, which takes control from there forward.

Your Main.eq should look like this:

class Main : SEApplication
{
	public Main() {
		SEDefaultEngine.activate();
	}

	public FrameController create_main_scene() {
		return(new MenuScene());
	}
}

NOTE: While making your game, please take effort to follow proper and standard code formatting, it will help you a lot!

EQ Coding Standards

Creating classes

If you try to click the "Run" button now, you will have an error (failed to resolve data type, pointing to the MenuScene). This means that there is currently no class named MenuScene in any of the files in your source code directory.

To fix this problem, we need to create a new file named MenuScene.eq that includes the MenuScene class, and save it to the source code directory. To do this, click on the "Project" tab on the menubar, then select "New Text File ..". A dialog box will be opened; enter "MenuScene.eq", then click "OK".

You will now have an empty file named "MenuScene.eq". In the next step, you will then create the actual class and its contents.

Creating and designing scenes

Creating a scene means creating a new class that represents a specific section of the overall game. The design or the elements added on each scene should be relevant to the purpose of the scene. There are several scenes that we need to create for our game.

MenuScene: Normally, every game would have a menu scene that serves as a starting point before the actual gameplay. Our MenuScene class will be first displayed upon starting the application. The MenuScene (and all other scenes) should extend SEScene.

public class MenuScene : SEScene
{
}

For our purposes, the scene will display an image that indicates the name of the game, placed at the mid-upper part of the screen. Three buttons labeled "Play", "How to play", and "Quit" will also be placed below the image.

Code for MenuScene

Since we are going to use an image for the name of the game, we will need to save the image file in the source code directory. We need to create a class for the MenuScene.

Place this code inside your MenuScene class

public class MenuScene : SEScene
{
	SESprite bg;
	SESprite game_name;
	SEButtonEntity btn_play;
	SEButtonEntity btn_howto;
	SEButtonEntity btn_quit;

	public void initialize(SEResourceCache rsc) {
		base.initialize(rsc);
		double w = get_scene_width();
		double h = get_scene_height();
		rsc.prepare_image("bg-image", "bg", w, h);
		rsc.prepare_font("name", "hurry up.ttf bold color=#c9731e", h * 0.20);
		rsc.prepare_font("pwnormal", "hurry up.ttf bold color=#c9731e", h * 0.12);
		rsc.prepare_font("pwpressed", "hurry up.ttf bold color=#bf6d1b", h * 0.12);
		rsc.prepare_font("pwhover", "hurry up.ttf bold color=#70300d", h * 0.12);
		bg = add_sprite_for_image(SEImage.for_resource("bg-image"));
		game_name = add_sprite_for_text("Catch It!", "name");
		add_entity(btn_play = SEButtonEntity.for_text("Play", "pwnormal", "pwhover", "pwpressed").set_data("play").set_listener(this));
		add_entity(btn_howto = SEButtonEntity.for_text("How to play", "pwnormal", "pwhover", "pwpressed").set_data("howto").set_listener(this));
		add_entity(btn_quit = SEButtonEntity.for_text("Quit", "pwnormal", "pwhover", "pwpressed").set_data("quit").set_listener(this));
		bg.move(0, 0);
		game_name.move(w * 0.5 - game_name.get_width() * 0.5, h * 0.15);
		btn_play.move(w * 0.5 - btn_play.get_width() * 0.5, h * 0.35);
		btn_howto.move(w * 0.5 - btn_howto.get_width() * 0.5, btn_play.get_y() + (btn_play.get_height()));
		btn_quit.move(w * 0.5 - btn_quit.get_width() * 0.5, btn_howto.get_y() + (btn_howto.get_height() * 1.15));
}

	public void on_message(Object o) {
		base.on_message(o);
		if("play".equals(o)) {
			switch_scene(new GameScene());
		}
		else if("howto".equals(o)) {
			switch_scene(new InstructionScene());
		}
		else if("quit".equals(o)) {
			SystemEnvironment.terminate(0);
		}
	}

	public void cleanup() {
		base.cleanup();
		bg = SESprite.remove(bg);
		game_name = SESprite.remove(game_name);
		btn_play.remove_entity();
		btn_howto.remove_entity();
		btn_quit.remove_entity();
	}
}

Above, we used entities and sprites to construct the scene. Our MenuScene looks like this:

InstructionScene: This scene contains instructions on how to play the game, the keys to be used, the goal that the user needs to achieve, and the like. We will also need to place a button that would take the user back to the MenuScene.

Code for InstructionScene

Create a new class for InstructionScene and place this code:

public class InstructionScene : SEScene
{
	SESprite bg;
	SESprite inst_line1;
	SESprite inst_line2;
	SESprite inst_line3;
	SESprite inst_line4;
	SESprite inst_line5;
	SEButtonEntity btn_back;

	public void initialize(SEResourceCache rsc) {
		base.initialize(rsc);
		double w = get_scene_width();
		double h = get_scene_height();
		rsc.prepare_image("bg-image", "bg", w, h);
		rsc.prepare_font("headerfont", "hurry up.ttf bold color=#70300d", h * 0.08);
		rsc.prepare_font("instructionfont", "hurry up.ttf bold color=#70300d", h * 0.05);
		rsc.prepare_font("pwnormal", "hurry up.ttf bold color=#c9731e", h * 0.12);
		rsc.prepare_font("pwpressed", "hurry up.ttf bold color=#bf6d1b", h * 0.12);
		rsc.prepare_font("pwhover", "hurry up.ttf bold color=#70300d", h * 0.12);
		add_entity(btn_back = SEButtonEntity.for_text("Back", "pwnormal", "pwhover", "pwpressed").set_data("back").set_listener(this));
		inst_line1 = add_sprite_for_text("To deliver eggs: ", "headerfont");
		inst_line2 = add_sprite_for_text("Press the truck or 'Spacebar' on the keyboard", "instructionfont");
		inst_line3 = add_sprite_for_text("To move the nest:", "headerfont");
		inst_line4 = add_sprite_for_text("Touch / click and drag using the mouse pointer", "instructionfont");
		inst_line5 = add_sprite_for_text("Press the 'left' or 'right' keys on the keyboard", "instructionfont");
		bg.move(0, 0);
		inst_line1.move(w * 0.05, h * 0.10);
		inst_line2.move(w * 0.10, inst_line1.get_y() + (inst_line2.get_height() * 1.75));
		inst_line3.move(w * 0.05, inst_line2.get_y() + (inst_line3.get_height() * 1.50));
		inst_line4.move(w * 0.10, inst_line3.get_y() + (inst_line4.get_height() * 1.75));
		inst_line5.move(w * 0.10, inst_line4.get_y() + (inst_line5.get_height() * 1.75));
		btn_back.move(w * 0.5 - btn_back.get_width() * 0.5, h - btn_back.get_height());
	}

	public void on_key_press(String name, String str) {
		if("escape".equals(name) || "back".equals(name)) {
			switch_scene(new MenuScene());
		}
	}

	public void on_message(Object o) {
		base.on_message(o);
		if("back".equals(o)) {
			switch_scene(new MenuScene());
		}
	}

	public void cleanup() {
		base.cleanup();
		bg = SESprite.remove(bg);
		inst_line1 = SESprite.remove(inst_line1);
		inst_line2 = SESprite.remove(inst_line2);
		inst_line3 = SESprite.remove(inst_line3);
		inst_line4 = SESprite.remove(inst_line4);
		inst_line5 = SESprite.remove(inst_line5);
		btn_back.remove_entity();
	}
}

The instruction scene, looks something like this:

GameScene: this is where the actual gameplay takes place:

(1) Add a full image background to the GameScene

(2) Add the texts indicating the current score (upper left of the screen), highscore (below the current score), game time (middle of the screen), total number of ordered eggs (below the game time), total number of eggs to miss (upper right below the Quit button)

(3) Add a button: "Quit" button that allows the user to go back to the MenuScene

(4) Add the entities: The chicken (positioned at the mid-upper part of the screen, and moves from left to right and vice versa), truck (positioned at the mid-left part), basket (positioned at the lower part of the screen, 20% at the top of the total height of the screen) and the eggs (randomly falls from the current position of the chicken and can be either fresh or rotten).

Code for GameScene

Create a new class for GameScene and place this code:

public class GameScene : SEScene
{
	SESprite bg;
	SESprite current_score;
	SESprite highscore;
	SESprite game_time;
	SESprite ordered_eggs;
	SESprite life;
	SEButtonEntity btn_quit;

	public void initialize(SEResourceCache rsc) {
		base.initialize(rsc);
		double w = get_scene_width();
		double h = get_scene_height();
		rsc.prepare_image("bg-image", "bg", w, h);
		rsc.prepare_image("truck", "truck", w * 0.25, h * 0.20);
		rsc.prepare_image("basket", "basket", w * 0.15, h * 0.12);
		rsc.prepare_font("gamefont", "hurry up.ttf bold color=#70300d", h * 0.04);
		rsc.prepare_font("pwnormal", "hurry up.ttf bold color=#c9731e", h * 0.06);
		rsc.prepare_font("pwpressed", "hurry up.ttf bold color=#bf6d1b", h * 0.06);
		rsc.prepare_font("pwhover", "hurry up.ttf bold color=#70300d", h * 0.06);
		rsc.prepare_image_sheet("chicken", null, 3, 1, 3, w * 0.15, h * 0.20);
		rsc.prepare_image_sheet("egg", null, 3, 2, 6, w * 0.05, h * 0.07);
		rsc.prepare_image_sheet("rotten", null, 3, 2, 6, w * 0.05, h * 0.07);
		bg = add_sprite_for_image(SEImage.for_resource("bg-image"));
		current_score = add_sprite_for_text("Score: 0", "gamefont");
		highscore = add_sprite_for_text("Highscore: %d".printf().add(SimpleHighScore.get_current()).to_string(), "gamefont");
		game_time = add_sprite_for_text("Time: %d".printf().add(time).to_string(), "gamefont");
		ordered_eggs = add_sprite_for_text("Order: 0/%d".printf().add(order).to_string(), "gamefont");
		life = add_sprite_for_text("To miss: 5", "gamefont");
		add_entity(btn_quit = SEButtonEntity.for_text("Quit", "pwnormal", "pwhover", "pwpressed").set_data("quit").set_listener(this));
		add_entity(chicken = new Chicken());
		add_entity(truck = new Truck());
		add_entity(basket = new Basket());
		bg.move(0, 0);
		current_score.move(0, 0);
		highscore.move(0, current_score.get_y() + current_score.get_height());
		game_time.move(w * 0.5 - game_time.get_width() * 0.5, 0);
		ordered_eggs.move(game_time.get_x(), game_time.get_y() + game_time.get_height());
		btn_quit.move(w - btn_quit.get_width(), 0);
		life.move(w - life.get_width(), btn_quit.get_y() + btn_quit.get_height());
	}

	public void on_message(Object o) {
		base.on_message(o);
		if("quit".equals(o)) {
			switch_scene(new MenuScene());
		}
	}

	public void on_key_press(String name, String str) {
		if("escape".equals(name) || "back".equals(name)) {
			switch_scene(new MenuScene());
		}
	}

	public void cleanup() {
		base.cleanup();
		bg = SESprite.remove(bg);
		current_score = SESprite.remove(current_score);
		highscore = SESprite.remove(highscore);
		game_time = SESprite.remove(game_time);
		ordered_eggs = SESprite.remove(ordered_eggs);
		life = SESprite.remove(life);
		clear_entities();
	}
}

As you noticed, we added the Chicken, Truck and Basket as entities. We will get to learn about this on the next topic. Our GameScene will look something like this:

GameOverScene: this scene is shown when the game is over. We will add a "Play again!" button that allows the user to go back to the GameScene and "Back to main menu" button that allows the user to go back to the MenuScene.

Code for GameOverScene

Create a new class for the GameOverScene and place this code:

public class GameOverScene : SEScene
{
	SESprite bg;
	SESprite img_gameover;
	SESprite total_score;
	SESprite highscore;
	SEButtonEntity btn_play_again;
	SEButtonEntity btn_back;

	public void initialize(SEResourceCache rsc) {
		base.initialize(rsc);
		double w = get_scene_width();
		double h = get_scene_height();
		rsc.prepare_image("bg-image", "bg", w, h);
		rsc.prepare_image("gameover", "gameover", w * 0.30, h * 0.25);
		rsc.prepare_font("gofont", "hurry up.ttf bold color=#c9731e", h * 0.15);
		rsc.prepare_font("tallyfont", "hurry up.ttf bold color=#c9731e", h * 0.10);
		rsc.prepare_font("pwnormal", "hurry up.ttf bold color=#c9731e", h * 0.06);
		rsc.prepare_font("pwpressed", "hurry up.ttf bold color=#bf6d1b", h * 0.05);
		rsc.prepare_font("pwhover", "hurry up.ttf bold color=#70300d", h * 0.06);
		bg = add_sprite_for_image(SEImage.for_resource("bg-image"));
		img_gameover = add_sprite_for_text("Gameover!", "gofont");
		total_score = add_sprite_for_text("Score: %d".printf().add(score).to_string(), "tallyfont");
		highscore = add_sprite_for_text("HighScore: %d".printf().add(get_highscore()).to_string(), "tallyfont");
		add_entity(btn_play_again = SEButtonEntity.for_text("Play again!", "pwnormal", "pwhover", "pwpressed").set_data("again").set_listener(this));
		add_entity(btn_back = SEButtonEntity.for_text("Back to main menu", "pwnormal", "pwhover", "pwpressed").set_data("back").set_listener(this));
		bg.move(0, 0);
		img_gameover.move(w * 0.5 - img_gameover.get_width() * 0.5, h * 0.3 - img_gameover.get_height() * 0.5);
		total_score.move(w * 0.5 - total_score.get_width() * 0.5, img_gameover.get_y() + img_gameover.get_height());
		highscore.move(w * 0.5 - highscore.get_width() * 0.5, total_score.get_y() + total_score.get_height());
		btn_play_again.move(w * 0.25 - btn_play_again.get_width() * 0.5, highscore.get_y() + highscore.get_height());
		btn_back.move(w * 0.75 - btn_back.get_width() * 0.5, btn_play_again.get_y());
	}

	public void on_message(Object o) {
		base.on_message(o);
		if("again".equals(o)) {
			switch_scene(new GameScene());
		}
		else if("back".equals(o)) {
			switch_scene(new MenuScene());
		}
	}

	public void cleanup() {
		base.cleanup();
		bg = SESprite.remove(bg);
		img_gameover = SESprite.remove(img_gameover);
		total_score = SESprite.remove(total_score);
		highscore = SESprite.remove(highscore);
		clear_entities();
	}
}

Now we have our GameOverScene. This scene will only show when the game is over. It looks something like this:

To go from one scene to another, we use a method from the SEScene class:

switch_scene(new GameScene());

Now we have all our scenes in place. You can change or improve the design for each scene, as you may want to.

Check out the API for SEScene under Jkop library.

The game loop

The game loop is the main driver of the game. It is a constant loop that, on every iteration, updates the game state and draws / updates the graphics. The basic game loop is automatically provided by Jkop (Sprite Engine), and can be extended by the programmer. In our implementation, then, inside each scene, we can implement the update() method. This method is called by the game loop repeatedly.

public void update(TimeVal now, double delta) {
	base.update(now, delta);
	// do something here
}

The "now" parameter tells you the current time; the "delta" parameter tells you how long has elapsed since the method was last called.

We will use this method in one of our scene later on.

Adding game resources

Game resources can include graphics, custom fonts, audio clips (usually sound effects) and music.

Please extract the contents of the resources zipped folder (that you downloaded earlier) in the source code directory of your project.

Image resources

You can easily incorporate PNG or JPG graphics in your Eqela games. However, on some platforms only PNG files are supported, so it is therefore recommended to prioritize PNG files. All bitmap graphics files (ending with either of the extensions .png or .jpg) in the source code directory (along with your .eq files) are considered "bitmap resources", and can be referenced from your code using their "id" (which is the name of the file without extension)

To incorporate bitmap graphics this way, simply copy your image resource files in the source directory. Open your project directory, then open the module name of your project, and place your resources together with your .eq files.

In our code, then, we need to prepare the image first before using it. We will prepare it in the initialize method of a scene class:

rsc.prepare_image("prepared_image", "resourceid", width, height);

The first parameter is the identifier of the image, the second parameter is the filename without extension, the third and fourth parameters are the width and height of the image, respectively.

To display the image on screen, then create a sprite for it using the prepared image:

sprite = add_sprite_for_image(SEImage.for_resource("prepared_image"));

NOTE: The identifier of the image must match the supplied id on the SEImage.for_resource() method.

A better way to address an image with fixed dimension

To ensure compatibility with different devices, screen sizes, resolutions and pixel densities, never directly write fixed pixel coordinates, positions or sizes in our code: Instead, use numbers that are relative to the current size of the screen.

We can use the methods of the SEScene class that return the current width and height of the screen: get_scene_width() and get_scene_height(). For computations, use floating point values (double data types) e.g.: creating image that is 10% of the total screen width and 15% of the total screen height:

double width = get_scene_width() * 0.10;
double height = get_scene_height() * 0.15;

Text resources

Elements of SESprite type that are in styled text form, are considered as labels. The same as with images, we need to first prepare a font before we can use it in our program. Again, we must prepare it in the initialize method:

rsc.prepare_font("myfont", "arial bold color=black", get_scene_height() * 0.05);

The first parameter is the font identifier, the second parameter is the style of the font (you can use any font currently installed, or you can download a custom font from the Internet; just make sure to include the downloaded font file in your source code directory). The third parameter is the size of the font.

Create a new sprite using the specified font and with the given text contents:

text = add_sprite_for_text("This is a sample text", "myfont");

The identifier of the prepared font should match to the second parameter of the add_sprite_for_text() method. You can now add image and font (text) resources on our game.

NOTE: As you noticed, we added image and text resources in each of our scenes earlier.

Audio resources

We will also add sound effects for the game.

NOTE: Audio file format support is very platform dependent. Different platforms support different formats, specifically:

* Windows Desktop : wav

* HTML5 : mp3 / ogg / m4a / wav (depending on the browser)

* Mac OS X : mp3

* Android : mp3 / ogg / m4a / wav

* J2ME : wav

To support all platforms, you need to supply the audio clip in the appropriate formats.

Audio clips

Meant for short clips like firing bullets, jumping, collisions with other objects and the like.

To start with, we need to copy the audio file on the source code directory. Then, audio clip playback can be incorporated through the use of the eq.audio.clip module / library. Like with other resources, we need to prepare the audio clip first. Place this code in the initialize method:

AudioClipManager.prepare("shoot");

The parameter of the prepare method of AudioClipManager is the name of the audio file without extension

To then play the audio clip:

AudioClipManager.play("shoot");

Audio player

AudioPlayer, as compared to AudioClip, is meant for longer audio data (commonly the background music of the game). Audio player playback is enabled through the use of the eq.audio.player module / library. As with audio clips, we first need to prepare the audio. Place this code in the initialize method:

AudioPlayerManager.prepare("background-music");

To use the audio:

AudioPlayerManager.play("background-music");

Unlike the AudioClip, we can pause, stop and loop the audio player. Check for those methods in the API.

User input

User input (both keyboard and pointer / mouse / touch screen) can be handled in two ways:

1. By overriding certain virtual methods of the SEScene to detect when the event starts (pressed) and stops (released)

Below are the methods of the SEScene class. Implement based on your need:

public void on_key_press(String name, String str) {
	base.on_key_press(name, str);
	// implementation goes here
}

public void on_key_release(String name, String str) {
	base.on_key_release(name, str);
	// implementation goes here
}

public void on_pointer_leave(SEPointerInfo pi) {
	base.on_pointer_leave(pi);
	// implementation goes here
}

public void on_pointer_press(SEPointerInfo pi) {
	base.on_pointer_press(pi);
	// implementation goes here
}

public void on_pointer_release(SEPointerInfo pi) {
	base.on_pointer_release(pi);
	// implementation goes here
}

public void on_pointer_move(SEPointerInfo pi) {
	base.on_pointer_move(pi);
	// implementation goes here
}

2. By checking for the status of keys or pointers during the update() method

public void update(TimeVal now, double delta) {
	base.update(now, delta);
	if(is_key_pressed("space")) {
		// do something that reacts to "space" being pressed
	}
	if(is_key_pressed("a")) {
		// do something that reacts to "a" being pressed
	}
	foreach(SEPointerInfo pi in iterate_pointers()) {
		if(pi.get_pressed()) {
			// The pointer or touchscreen is being touched / clicked
			// get the pointer coordinates through pi.get_x() and pi.get_y()
		}
	}
}

Let's understand the methods used:

(1) is_key_pressed("") is a method that accepts a String parameter, which can be the name of the key or the character on the keyboard.

(2) "foreach" is a special loop that iterates through a collection.

(3) iterate_pointers() is a method of the SEScene class that returns a collection. The collection it returns contains the coordinates of the pointer.

(4) get_pressed() is a method of SEPointerInfo class which returns true if the pointer is pressed and false if otherwise.

With this, now we can add life to our game!

Preparing and adding elements on the scene

We will also add elements to the scene like buttons, players, monsters, labels, layers and the like. Adding of elements in the scene is normally done in the initialize method, although it can be done elsewhere in the program, whenever you need to add an element.

Sprites

Sprites are the basic building blocks of 2D games. An element of the SESprite type can be one of three forms: It can be either solid color, styled text, or a bitmap image.

For solid color sprites:

sprite = add_sprite_for_color(Color.instance("#FFA500"), get_scene_width(), get_scene_height());

For styled text:

text_sprite = add_sprite_for_text("Display text", "prepared_font_identifier");

For bitmap images:

image_sprite = add_sprite_for_image(SEImage.for_resource("prepared_image_identifier"));

Entities

Entities (instances of SEEntity class) can be added to a scene in a manner very much like other elements (layer or sprite) can. However, entities do not have appearance. They have behavior.

Sprite Entities

A sprite entity is a specialized form of SEEntity. Players, monsters, bullets and the like are declared in different classes, and each extends SESpriteEntity.

public class MyPlayer : SESpriteEntity
{
	public void initialize(SEResourceCache rsc) {
		base.initialize(rsc);
		set_image(SEImage.for_resource("prepared_image_identifier"));
		move(0, 0);
	}

	public void tick(TimeVal now, double delta) {
		base.tick(now, delta);
		// behavior of the player here
	}
}

As you have noticed, we directly call the set_image() method of the SESprite class without creating an instance of SESprite. We also use the move() method of SEElement without creating an instance of it. Why is this possible?

Check the SESpriteEntity class, and see the answer for yourself.

For this class, we use the tick() method. It is the same as the update() method of the SEScene class.

Now that we know that, we need to create the Truck and Basket entities.

Code for the Truck

Create a new class for the Truck entity and place this code:

public class Truck : SESpriteEntity
{
	public void initialize(SEResourceCache rsc) {
		base.initialize(rsc);
		set_image(SEImage.for_resource("truck"));
		move(0, get_scene_height() * 0.60);
		set_alpha(0.5);
	}

	public void tick(TimeVal now, double delta) {
		base.tick(now, delta);
	}

	public void cleanup() {
		base.cleanup();
	}
}

Code for the Basket

Create a new class for the Basket entity and place this code:

public class Basket : SESpriteEntity
{
	public void initialize(SEResourceCache rsc) {
		base.initialize(rsc);
		set_image(SEImage.for_resource("basket"));
		move(get_scene_width() * 0.25, get_scene_height() * 0.80);
	}

	public void cleanup() {
		base.cleanup();
	}
}

Sprite Sheets

A sprite sheet is a collection of related images (frames) that are saved as one big image, usually arranged in a grid. They are often used to provide the frames of a complete animation as a single image file.

Below are the sprite sheet images for the egg and the chicken:

To use sprite sheets, just like image resources, we need to add the image file to our source directory. We also need to use a timer for the animation.

class Hen : SESpriteEntity, SEPeriodicTimerHandler
{
	int cols_count = 3;
	int row_count = 4;
	int total_no_of_images = 12;

	public void initialize(SEResourceCache rsc) {
		base.initialize(rsc);
		rsc.prepare_image_sheet("hen", null, cols_count, row_count, total_no_of_images, get_scene_width() * 0.1, get_scene_height() * 0.1);
		set_image_sheet(rsc.get_image_sheet("hen"));
		add_entity(SEPeriodicTimer.for_handler(this, 50000));
		move(0, 0);
	}

	public bool on_timer(TimeVal now) {
		next_frame();
		return(true);
	}
}

To prepare the image sheet, we use the prepare_image_sheet() method of the SEResourceCache class that has 7 parameters: filename of the image sheet without extension (String), resourceid that can be null (String), number of columns (int), number of rows (int), total number of images in the sheet (int), width (double), height (double) respectively.

To set the image sheet as the image for the sprite, we use set_image_sheet() method of the SESpriteEntity class, that requires one parameter of array type.

We called the get_image_sheet() method of the SEResourceCache class with the first parameter of the prepare_image_sheet() method. This method returns array that's why we supplied it to the set_image_sheet() method.

Inside the on_timer() method, we call the next_frame() method of the SESpriteEntity that enables the image to move from one frame to the other.

For the Chicken and Egg entity we will use sprite sheets for them.

Code for Chicken Entity

Create a new class for the Chicken entity and place this code:

public class Chicken : SESpriteEntity, SEPeriodicTimerHandler
{
	public void initialize(SEResourceCache rsc) {
		base.initialize(rsc);
		set_image_sheet(rsc.get_image_sheet("chicken"));
		add_entity(SEPeriodicTimer.for_handler(this, 100000));
		move(get_scene_width() * 0.5 - get_width() * 0.5, get_scene_height() * 0.15);
	}

	public bool on_timer(TimeVal now) {
		next_frame();
		return(true);
	}

	public void cleanup() {
		base.cleanup();
	}
}

Code for Egg entity

Create a new class for the Egg entity and place this code:

public class Egg : SESpriteEntity, SEPeriodicTimerHandler
{
	public void initialize(SEResourceCache rsc) {
		base.initialize(rsc);
		set_image_sheet(rsc.get_image_sheet("egg"));
		add_entity(SEPeriodicTimer.for_handler(this, 50000));
	}

	public bool on_timer(TimeVal now) {
		next_frame();
		return(true);
	}

	public void cleanup() {
		base.cleanup();
	}
}

Button Entities

SEButtonEntity is a specialized type of SESpriteEntity that is meant for creating buttons. These buttons can be in text or image form.

Just like any other sprite, we need to create the entity object before we can use it. We need to declare a member variable of SEButtonEntity type, initialize a value for it and add it to the scene.

Since SEButtonEntity is meant for creating buttons, we just need to specify the "data" or the triggering event and the "listener" of the event. Then we need to implement "on_message()" method that is called when the button is clicked.

class GameScene : SEScene
{
	SEButtonEntity play_btn;

	public void initialize(SEResourceCache rsc) {
		base.initialize(rsc);
		// Prepare the font or image for the button
		// For this purpose we will use text buttons
		// so we need to prepare the "myfont" first
		add_entity(play_btn = SEButtonEntity.for_text("Play", "myfont")
			.set_data("play").set_listener(this));
		// call the move method to position the button
	}

	public void on_message(Object o) {
		base.on_message(o);
		if("play".equals(o)) {
			// go to the GameScene
			return;
		}
	}
	
}

Other Entities

There are also other specialized entities that you can use in your game. You can check the API for the available methods that you can use for these classes.

SEMovingSpriteEntity

A specialized SESpriteEntity type, it can move around automatically, and can also remove itself from the scene when it reaches the end of its movement.

SEFallingSpriteEntity

A specialized SESpriteEntity type where you can set the acceleration property of the sprite. Check the API reference for the properties and methods of these classes.

After creating the entities, we will add them to the scene by calling the method of the SEScene class and the class name of the entity as the parameter.

Add the newly created entities in the GameScene. Create member variables for these entities:

SESpriteEntity chicken;
SESpriteEntity truck;
SESpriteEntity basket;

Insert these lines of code inside the initialize method after the last line.

add_entity(chicken = new Chicken());
add_entity(truck = new Truck());
add_entity(basket = new Basket());

NOTE: We will not yet add the Egg entity.

Entities do not need to be manually removed from the scene, but if you want to remove it you can use:

remove_entity();

Timers

We can add timers that can control the game, and we can also make use of this when we want to add several elements periodically, or if we would want to destroy an element eg. on every half a second.

Periodic timers can be easily implemented with the convenience of SEPeriodicTimer class: We need to inherit SEPeriodicTimerHandler using a comma (,) after the base class, as seen below. Since SEPeriodicTimerHandler is an interface, we need to implement the on_timer() method in the class where we inherit it. We also need to set the interval of the timer by adding it to the class, much like how we add entities:

class GameScene : SEScene, SEPeriodicTimerHandler
{
	public void initialize(SEResourceCache rsc) {
		base.initialize(rsc);
		add_entity(SEPeriodicTimer.for_handler(this, 1000000));
	}

	public bool on_timer(TimeVal now) {
		// implementation here
		return(true);
	}
}

The first parameter of the for_handler() method is the handler, we use "this" because we implement the SEPeriodicTimerHandler in this class. The second parameter is the delay expressed in microsecond. The on_timer() method must return "true" to continue the timer, return false to end it.

Layers

Layers can be used to group sprites or entities together, and to control their stacking order. To use layers, we must create the layer first, add it to the scene and then add elements inside the layers.

(1) Create an SELayer object as member variable:

SELayer text_layer;

(2) Add the layer to the scene, place this code inside the initialize method:

text_layer = add_layer(0, 0, get_scene_width(), get_scene_height());

The add_layer() method has four parameters, the x, y, width and height of the layer (respectively)

(3) Add elements to the layer:

text_sprite = text_layer.add_sprite_for_text("This is a text sprite", "myfont");

Reminder: Don't forget to clean up

Any sprite you create during your gameplay, you MUST destroy in the cleanup() method of your game. If your entity or scene creates a sprite, you must remove it in the cleanup: If you do not, the sprite may "linger", and consecutive gameplays will become slower and slower (this is a frequently asked question).

To clean up, we need to implement the cleanup() method in each of our classes:

public void cleanup() {
	base.cleanup();
	// for sprites
	sprite = SESprite.remove(sprite);
	// for layers 
	layer = SELayer.remove(layer);
}

Adding game functionality

We will start the game time and display the current elapsed time of the game (the timer will start).

Code for display of game time

We need to add time to our game.

(1) Let the GameScene inherit the SEPeriodicTimerHandler interface.

public class GameScene : SEScene, SEPeriodicTimerHandler

(2) Create a member variable of integer type that will hold the time, give it an initialize value of 60.

int time = 60;

(3) Display the time using the text sprite that we created earlier in the GameScene class. Instead of using a static text, we will concatenate the value of the time variable.

Change this line of code:

game_time = add_sprite_for_text("Time: 60", "gamefont");

to this:

game_time = add_sprite_for_text("Time: %d".printf().add(time).to_string(), "gamefont");

(4) We will need to add the handler for our timer. Add this line of code in the initialize method

add_entity(SEPeriodicTimer.for_handler(this, 1000000));

(5) Implement the on_timer() method. Inside this method decrease the value of the time variable and display it using the text sprite. Insert this code after the initialize method.

public bool on_timer(TimeVal now) {
	time--;
	game_time.set_text("Time: %d".printf().add(time).to_string());
	return(true);
}

Now we have our time running.

Now we would want the chicken to move from left to right and vice versa.

Code for the Chicken movement

The chicken should move from left to right and vice versa (without user intervention/control)

The chicken can only move in between 25% until 95% of the screen width and vice versa. We need to add code on the Chicken Entity class for its behavior.

(1) Create member variables of double data types for the left and right boundary.

double left_boundary;
double right_boundary;

(2) Initialize values for the newly created variables. Place the following code in the initialize method of the Chicken Entity class

left_boundary = get_scene_width() * 0.25;
right_boundary = get_scene_width() * 0.95;

(3) We will randomize the movement of the Chicken and its animation, in order to do that we will create inner classes inside our Chicken class. Place this inside the Chicken class, before the declaration of member variables.

class ChickenMovement : SEPeriodicTimerHandler
{
	property Chicken c;
	
	public bool on_timer(TimeVal now) {
		c.randomize_chicken_movement();
		return(true);
	}
}

class ChickenAnimation : SEPeriodicTimerHandler
{
	property Chicken c;

	public bool on_timer(TimeVal now) {
		c.animate();
		return(true);
	}	
}

Our Chicken class will not anymore inherit SEPeriodicTimerHandler, the animation will be handled by the inner classes. Inside our initialize class, we will remove:

add_entity(SEPeriodicTimer.for_handler(this, 100000));

Replace it with:

add_entity(SEPeriodicTimer.for_handler(new ChickenAnimation().set_c(this), 100000));
add_entity(SEPeriodicTimer.for_handler(new ChickenMovement().set_c(this), 600000));

We will also need to remove the on_timer() method, and create new methods for the movement and animation of the Chicken:

public void randomize_chicken_movement() {
	int rand_direction = Math.random(1, 5);
	if(rand_direction%2 == 0) {
		is_moving_right = true;
		return;
	}
	is_moving_right = false;
}

public void animate() {
	next_frame();
}

(4) In our tick() method we will implement the movement:

public void tick(TimeVal now, double delta) {
	base.tick(now, delta);
	if(is_moving_right) {
		move(get_x() + delta * 300, get_y());
		if(get_x() + get_width() > right_boundary) {
			is_moving_right = false;
		}
	}
	else {
		move(get_x() - delta * 300, get_y());
		if(get_x() < left_boundary) {
			is_moving_right = true;
		}
	}
}

That's it!

The basket at the bottom, also moves from left to right and vice versa via user input (or user control)

Code for the Basket to move

The basket should move from left to right, the same as the Chicken but with user intervention (user control)

The basket should move using arrow keys (for deskop) and pointer (for desktop) / touch click and drag (for mobile devices)

We will need to add code to our Basket Entity for its behavior. The same as the Chicken Entity, we will also set boundary for the Basket

(1) Create member variables of double data type for left and right boundary

double left_boundary;
double right_boundary;

(2) Initialize values for the newly created variables. Place this code inside the initialize method of the Basket Entity class

left_boundary = get_scene_width() * 0.25;
right_boundary = get_scene_width() * 0.95;

(3) Now, we will implement the tick() method for the movement of the basket using key presses

public void tick(TimeVal now, double delta) {
	base.tick(now, delta);
	double xcoor = get_x();
	if(is_key_pressed("left") && xcoor > left_boundary) {
		xcoor -= delta * 500;
	}
	else if(is_key_pressed("right") && xcoor + get_width() < right_boundary) {
		xcoor += delta * 500;
	}
	move(xcoor, get_y());
}

(5) We will also let the basket move through pointer / touch click and drag. To do this, we need to inherit SEPointerListener interface

public class Basket : SESpriteEntity, SEPointerListener

(6) Implement the on_pointer_press method, check if the pointer is inside the basket

public void on_pointer_press(SEPointerInfo pi) {
	if(pi.is_inside(get_x(), get_y(), get_width(), get_height())) {
		pointerid = pi.get_id();
		x = pi.get_x() - get_x();
	}
}

Declare the "pointerid" that should be of integer type and initialize it to -1 and "x" that should be of double data type

(7) Implement the on_pointer_move method to make the basket move

public void on_pointer_move(SEPointerInfo pi) {
	if(pointerid < 0 || pi.get_id() != pointerid) {
		return;
	}
	move(pi.get_x() - x, get_y());
}

(8) And on_pointer_release to stop the movement of the basket upon release of the pointer

public void on_pointer_release(SEPointerInfo pi) {
	if(pointerid >= 0 && pointerid == pi.get_id()) {
		pointerid = -1;
	}
}

Now, our basket can move.

The eggs falls randomly from the current position of the chicken going downwards and can be either fresh or rotten.

Code for the Egg entity

The egg should fall from the current position of the Chicken going downwards and it can be either fresh or rotten

We should prepare the images (in the GameScene) that is needed for the Egg entity (fresh and rotten image of the egg).

In the previous section, we initially create our Egg entity, now we are going to modify it so to conform with the requirements of the game.

(1) We will create a method that will set the type of the Egg. Place this method below the initialize method

int create_egg_type() {
	int set = Math.random(0, 2);
	if(set%2 == 0) {
		return(1);
	}
	return(0);
}

(2) In the initialize method, we will call the create_egg_type() method before setting the image of the egg (to know what the egg will be fresh or rotten). Modify the initialize method of the Egg entity class.

public void initialize(SEResourceCache rsc) {
	base.initialize(rsc);
	type = create_egg_type();
	if(type == 1) {
		set_image_sheet(rsc.get_image_sheet("egg"));
	}
	else {
		set_image_sheet(rsc.get_image_sheet("rotten"));
	}
	add_entity(SEPeriodicTimer.for_handler(this, 50000));
}

Declare the "type" as property variable of integer type.

(3) We will implement the tick() method and make the egg move downwards

public void tick(TimeVal now, double delta) {
	base.tick(now, delta);
	move(get_x(), get_y() + delta * 100);
	if(get_y() > get_scene_height()) {
		remove_entity();
	}
}

Now we have that in place, we will add the Egg entity on the current position of the Chicken, we will do that on the GameScene. We will create a method that will add eggs to the scene.

public void lay_egg() {
	if(chicken != null) {
		add_entity(new Egg().set_xy(chicken.get_x() + chicken.get_width() * 0.5, chicken.get_y() + chicken.get_height() * 0.55));
	}
}

We need to call the lay_egg method to add egg to the scene. The egg will fall randomly in order to do that we will create inner classes that will handle the adding of eggs and the game time.

Place this inner classes inside the GameScene before the declaration of the member variables:

class GameTimer : SEPeriodicTimerHandler
{
	property GameScene gs;

	public bool on_timer(TimeVal now) {
		gs.display_time_status();
		return(true);
	}
}

class EggTimer : SEPeriodicTimerHandler
{
	property GameScene gs;

	public bool on_timer(TimeVal now) {
		gs.lay_egg();
		return(true);
	}
}

Replace:

add_entity(SEPeriodicTimer.for_handler(this, 1000000));

With this:

add_entity(SEPeriodicTimer.for_handler(new GameTimer().set_gs(this), 1000000));
add_entity(SEPeriodicTimer.for_handler(new EggTimer().set_gs(this), 800000));

We will also remove the on_timer() method and replace it with this:

public void display_time_status() {
	time--;
	game_time.set_text("Time: %d".printf().add(time).to_string());
}

When a fresh egg falls on the boundary or collides on the basket, it will be counted as one. If a rotten egg falls on the basket, it will decrease the current value of the caught eggs.

Collision of Egg and Basket

From this point, we have the basket, chicken and falling eggs in place. We will now need to check if the basket catches the egg.

(1) In our GameScene, we will create member variables of integer type that will hold the value for the catch and ordered eggs which will be initially be 0, another member variable that will hold the allowed number of eggs to miss; and another property member variable that will hold the current score.

int catched = 0;
int order = 0;
int eggs_to_miss = 5;
property int score = 0;

Replace the value of ordered_eggs sprite to:

ordered_eggs = add_sprite_for_text("Order: 0/%d".printf().add(order).to_string(), "gamefont");

And then call the create_order() method of the GameScene.

(2) Inside the update method in the GameScene, we will check all the eggs that are added on the scene

public void update(TimeVal now, double delta) {
	base.update(now, delta);
	foreach(Egg e in get_entities()) {
		// do something here
	}
}

(3) Inside the foreach method, we will check if the egg reaches the basket

if(e.get_y() + e.get_height() > basket.get_y() + basket.get_height() * 0.5 && e.get_x() > basket.get_x() && e.get_x() + e.get_width() < basket.get_x() + basket.get_width()) {
	// Catched egg!
}

(4) Since there are two types of egg, if the condition (collision) statement returns true, then we will check if the caught egg is fresh or rotten. Now, we will make use of the "type" variable of the Egg entity class to check the type of egg

(4a) If the "type" returns 1, it means the egg is fresh, then we will check if number of catched eggs is less than the value of ordered eggs and then we will increment the catched variable. Place this code inside the if block

if(e.get_type() == 1 && catched < order) {
	catched++;
}

(4b) If the type of egg is rotten, we will then need to decrease the catched variable, we'll just put an else to the if statement that we have earlier

else {
	catched--;
}

(4c) We will also need to check if the value of the catched variable is equal to or exceeds to the number of ordered eggs, then it should not count the caught fresh egg instead it will decrease the eggs to miss. Place this code after the if-else block earlier (4a and 4b)

if(e.get_type() == 1 && catched >= order) {
	eggs_to_miss--;
}

(4d) Since we are decrementing the value of the catched variable, we will also need to take note that the value must not be negative. Place this code after the if block (4c)

if(catched <= 0) {
	catched = 0;
}

(5) After that, we will remove the egg from the scene.

e.remove_entity();

(6) We will also need to check if the basket failed to catch a fresh egg, then we will decrement the number of eggs to miss.

if(e.get_type() == 1 && e.get_y() + e.get_height() > get_scene_height() - (get_scene_height() * 0.10)) {
	eggs_to_miss--;
	e.remove_entity();
	return;
}

(7) We will need to display the current stats using the text sprites that we created. Place this lines of code inside the update method:

life.set_text("To miss: %d".printf().add(eggs_to_miss).to_string());
ordered_eggs.set_text("Order: %d/%d".printf().add(catched).add(order).to_string());
current_score.set_text("Score: %d".printf().add(score).to_string());

Check the current number of caught eggs: When it reach the required number of ordered eggs, the truck will be in full color (non-transparent color).

When the player taps the truck or hits the spacebar, 1 point will be counted as score, the eggs will be delivered and a new order of eggs will be displayed. The truck will again be 50% transparent.

Check for orders

We will indicate if the number of ordered eggs is reached using the truck.

(1) Inside the update method of the GameScene, we need to check if the catched eggs is equal to the ordered eggs

if(catched == order) {
	reached_order = true;
}	

Declare "reached_order" as a property member variable of boolean type

(2) On the tick() method of the Truck class we will check if the player reached the order via "reached_order" property variable of the GameScene, if the variable is true that means the order is reached and the truck should be in full color

(2a) For us to be able to call or use property variables and methods of the GameScene, we need to create an instance of the GameScene class using get_scene() method. Declare a member variable "gs" of GameScene type

GameScene gs;

(2b) Inside the initialize method, we need to initialize the value of "gs" to GameScene using the get_scene() method

gs = get_scene() as GameScene;

(2c) Now that we have an instance of the GameScene class, we can now get the value of the property variable "reached_order"

public void tick(TimeVal now, double delta) {
	base.tick(now, delta);
	if(gs.get_reached_order()) {
		set_alpha(1.0);
	}
}	

(3) We need to implement the SEPointerListener interface so we can make use of the on_pointer_press method

public class Truck : SESpriteEntity, SEPointerListener

(3a) When the truck is clicked, new order will be created, transparency of the truck will be 50% again and the reached_order variable will be set to false

public void on_pointer_press(SEPointerInfo pi) {
	if(gs.get_reached_order() == true && pi.is_inside(get_x(), get_y(), get_width(), get_height())) {
		set_alpha(0.5);
		gs.create_order();
		gs.set_reached_order(false);
		gs.set_score(gs.get_score() + 1);
	}
} 

Pressing the spacebar will also give it the same result, add this line of code to the tick() method:

if(is_key_pressed("space") == true && gs.get_reached_order() == true) {
	set_alpha(0.5);
	gs.create_order();
	gs.set_reached_order(false);
	gs.set_score(gs.get_score() + 1);
}

Inside the update method, you will also need to implement the other two methods that is required to be implemented

public void on_pointer_move(SEPointerInfo pi) {
}

public void on_pointer_release(SEPointerInfo pi) {
}

(4) Inside the GameScene class, we need to add create_order method that will generate new order and will also equate the catched variable to 0

public void create_order() {
	catched = 0;
	order = Math.random(1, 12);
	ordered_eggs.set_text("Order: 0/%d".printf().add(order).to_string());
}

When the player has missed a total of 5 fresh eggs or the time runs out, the game will be over.

When the game is over from the GameScene, it will automatically switch the current scene to the GameOverScene.

Add this line of code inside the update method of the GameScene. In the GameOverScene, we need to create a property member variable "score" of integer type that will hold the current score from the GameScene.

if(time == 0 || eggs_to_miss == 0) {
	switch_scene(new GameOverScene().set_score(score));
}

Now we ultimately have our game! :)

Storing game scores

We can store score information by using the SimpleHighScore class of the Jkop library (or you can easily create your own class if you like). To use SimpleHighScore, we need to add a dependency in the config file:

depends: jkop.highscore

If you check the API of the SimpleHighScore class, there are only two methods, which are both static. Static methods are invoked using the name of the class.

To get the current score of the player we need to call the get_current() method:

int current_highscore = SimpleHighScore.get_current();

If the current score of the player is higher than the current highscore, then we need to update the stored highscore by calling the update() method:

if(current_score > current_highscore) {
	SimpleHighScore.update(current_score);
}

In the GameOverScene:

property int score = 0;

Create a method that will check if the current score is greater than the highscore and will replace it:

int get_highscore() {
	int current_highscore = SimpleHighScore.get_current();
	if(score > current_highscore) {
		current_highscore = score;
	}
	SimpleHighScore.update(current_highscore);
	return(current_highscore);
}

In the initialize method of our GameOverScene, replace:

total_score = add_sprite_for_text("Score: 0", "gofont");
highscore = add_sprite_for_text("HighScore: 0", "gofont");

with:

total_score = add_sprite_for_text("Score: %d".printf().add(score).to_string(), "gofont");
highscore = add_sprite_for_text("HighScore: %d".printf().add(get_highscore()).to_string(), "gofont");

Your game is complete

Now we have our catching game, and it's ready to play! Enhance it, do more, and make it better. Yes you can!



Twitter Facebook LinkedIn Youtube Slideshare Github