Tutorial #00 - Installation

The nCine offers an extensive Lua API that covers most of the native C++ API and allows you to create a complete 2D game.

Choose the right artifact branch

To run your Lua games you need to download the nCine. The specific Lua version is recommended in this case as it is easier to setup.

You can find all the pre-compiled archives on GitHub, as branches of the same nCine Artifacts repository.

The branch name format is nCine-branch-platform-compiler: https://github.com/nCine/nCine-artifacts/branches/all

The Lua edition is called nCineLua and you usually want the master branch. For example, if you are on Windows, you can choose the nCine-master-windows-vs2022 branch (compiled with the latest Visual Studio 2022) and download the archive or the installer for the nCineLua version.

Download the archive

Inside the branch you will find an archive, its format depends on the choosen platform.

You can extract the archive wherever you want and that’s the only installation step needed.

If you are inside a Windows branch you will also find an installer. The nCine does not need to be installed though, you can download the ZIP archive.

Run the example script

Inside the extracted directory you will find an binary and a data directory.

Inside the binary directory you will find some libraries and an executable.

When you run the executable it will look for a file named script.lua, first in its same directory and then in the data one.

You can put your script.lua in either directories or you can execute a different script. To do that open a command prompt and give the path of your script as the first parameter.

For your data files (like textures or sounds), you can use an absolute path or a path relative to the executable directory. You can also put them in the data directory and use the default data path value returned by ncine.fs.get_data_path().

Additional resources

For a complete Lua API documentation, check the LDoc one at: https://ncine.github.io/docs/lua_master/

The nCine wiki offers further information: Getting Started with Lua and Getting Started with Lua (VSCode).

Tutorial #01 - Callbacks and mouse

Basic application callbacks

One of the first concepts to learn is the life cycle of your application and the associated event callbacks.

Callback functions are automatically called by the framework in particular moments during the life of the game, for example at the beginning of a frame or at shutdown.

Let’s write a small example that uses them.

nc = ncine

function nc.on_init()
	texture_ = nc.texture.new(nc.fs.get_data_path().."textures/texture2.png")

	local resolution = nc.application.get_resolution()
	pos_ = {x = resolution.x * 0.5, y = resolution.y * 0.5}
	sprite_ = nc.sprite.new(nc.application.get_rootnode(), texture_, pos_)

	angle_ = 0
end

function nc.on_frame_start()
	angle_ = angle_ + 2 * nc.application.get_interval()

	local radius = nc.application.get_width() * 0.2
	local newpos = {x = pos_.x + radius * math.sin(angle_), y = pos_.y + radius * math.cos(angle_)}
	nc.sprite.set_position(sprite_, newpos)
end

function nc.on_shutdown()
	nc.sprite.delete(sprite_)
	nc.texture.delete(texture_)
end

The first line is just a way to create a shorter alias for the ncine table: the API access point.

on_init() and on_shutdown()

The on_init()`function is called only once, at the beginning of your application. Here we are creating a new texture and a new sprite using the special `new() function.

The on_shutdown() function is its mirror, it is called before the application quits and should be used to free any resources created for the game: there is one delete() for each new() we called earlier.

All functions that work on objects created with a new() need those objects as their first parameter, and delete() is no exception.

To create a texture object we just need the filename of an image file. You can use an absolute path but in this example we are using the data/ directory by asking the filesystem table fs for the default data path.

Next we create a sprite object by passing four parameters to the sprite.new() function: a parent node, a texture object, and the X and Y coordinates of the initial position.

As a parent node we use the root node of the scenegraph by calling the get_rootnode() function of the application table. Every sprite that you want to display on screen needs to be a child of this node or to have an ancestor that is a child of this node.

The other function from the application table, get_resolution, returns the resolution of the screen and it is used here to place the sprite at the center of the screen.

on_frame_start()

The on_frame_start() function is where most of the game logic happens, it is called once at the beginning of each frame and allows you to update objects, like move sprite arounds, play sounds, and so on.

In this example we move the sprite in a circle path by using some simple trigonometry and the Lua math library.

We increment the angle_ variable and use it to update the sprite position with the sprite.set_position() function. By using the interval time passed since last frame we keep a stable rotation regardless of the frame time oscillations.

on_pre_init()

We can make the example a bit more flexible by configuring some aspects before our application or the framework even initialize.

For this we can use the on_pre_init() callback. It receives a configuration table that we can modify before returning it.

function nc.on_pre_init(cfg)
	cfg.resolution = {x = 1280, y = 720}
	cfg.window_title = "nCine example"
	return cfg
end

There are many more variables in this table, but for this example we are just setting the initial window resolution and title.

Mouse input

on_mouse_button_released()

Even if we a have a moving sprite on screen, this example is still boring: we cannot interact with it.

Let’s remedy to this by adding the ability to move the sprite around with the mouse.

function nc.on_mouse_button_released(event)
	if event.button == nc.mouse_button.LEFT then
		pos_ = {x = event.x, y = event.y}
	end
end

The on_mouse_button_released() function is called every time the user releases a mouse button. It receives an event table with additional information, like which button was released.

We are performing this check by comparing the button variable with the mouse_button.LEFT constant value. Those special constant values are usually written with capital letters.

If the user released the left button, we update the pos_ variable that will be used as the center of the rotation animation in the on_frame_start() function.

The coordinates of the mouse at the time a button was released are accessible in the x and y variables of the same table.

on_mouse_moved()

You will soon discover that smashing the mouse button is not an optimal way for continuously updating the sprite position. For this you need a different function.

The on_mouse_moved() function is called every time the mouse is moved and receives a state table that you can query in a way similar to the event one of on_mouse_button_released().

function nc.on_mouse_moved(state)
	if state.left_pressed then
		pos_ = {x = state.x, y = state.y}
	end
end

In this snippet we first check if we are moving the mouse while pressing the left mouse button by checking the value of the left_pressed boolean variable, then we use the x and y variables to update the position.

Putting it all together

Complete example #01
nc = ncine

function nc.on_pre_init(cfg)
	cfg.resolution = {x = 1280, y = 720}
	cfg.window_title = "nCine example"
	return cfg
end

function nc.on_init()
	texture_ = nc.texture.new(nc.fs.get_data_path().."textures/texture2.png")

	local resolution = nc.application.get_resolution()
	pos_ = {x = resolution.x * 0.5, y = resolution.y * 0.5}
	sprite_ = nc.sprite.new(nc.application.get_rootnode(), texture_, pos_.x, pos_.y)

	angle_ = 0
end

function nc.on_frame_start()
	angle_ = angle_ + 2 * nc.application.get_interval()

	local radius = nc.application.get_width() * 0.2
	local newpos = {x = pos_.x + radius * math.sin(angle_), y = pos_.y + radius * math.cos(angle_)}
	nc.sprite.set_position(sprite_, newpos)
end

function nc.on_shutdown()
	nc.sprite.delete(sprite_)
	nc.texture.delete(texture_)
end

function nc.on_mouse_button_released(event)
	if event.button == nc.mouse_button.LEFT then
		pos_ = {x = event.x, y = event.y}
	end
end

function nc.on_mouse_moved(state)
	if state.left_pressed then
		pos_ = {x = state.x, y = state.y}
	end
end

Tutorial #02 - Text

Bitmap fonts

With the latest tutorial you learnt how to load a texture and create a sprite from it. In this one you will see how to load a font and display some text on screen.

nCine uses bitmap fonts: they are special textures that contains all the characters. The information about how to render a text from those textures is contained in a FNT file. It is a text file that tells where every character is located inside the texture.

To create a font texture and the corresponding FNT description file you can use the Bitmap Font Generator or other tools that support AngelCode’s format.

Font and text node objects

To create a new font object you can use the font.new() function. As usual, remember to call font.delete() when you are done with it.

The parameters for this function are the path to a FNT file and the path to a texture file (be careful, it is not a texture object).

To display some text on screen you also need a text node. Just like a sprite, it has to be a child of the root node or have an ancestor that is a child of the root node.

You also need to associate it with the font to use: textnode.new(rootnode, font, 256).

The last parameter is the maximum length allowed for the text to be displayed. You can tune this value around the use you want to do of your text node, to optimize performance and memory usage.

nc = ncine

function nc.on_init()
	local rootnode = nc.application.get_rootnode()

	font_ = nc.font.new(nc.fs.get_data_path().."fonts/DroidSans32_256.fnt",
	                    nc.fs.get_data_path().."fonts/DroidSans32_256.png")

	local resolution = nc.application.get_resolution()
	textnode_ = nc.textnode.new(rootnode, font_, 256)
	nc.textnode.set_position(textnode_, resolution.x * 0.5, resolution.y * 0.5)
end

function nc.on_frame_start()
	nc.textnode.set_string(textnode_, string.format("FPS: %.2f", 1.0 / nc.application.get_interval()))
end

function nc.on_shutdown()
	nc.textnode.delete(textnode_)
	nc.font.delete(font_)
end

Additional text node functions

Dynamic size and position

We can make this example even better by positioning the text in one of the screen’s corners.

To do that we need to know how tall and how wide is the text that we are rendering. We can do exactly that with the get_width() and the get_height() functions.

If we are changing the string each frame, like in this example, we need to reposition the text each frame to take into account the changes in size.

Color

Changing the color of a text node is simple: you just have to call the set_color() function with the four color channel values (red, green, blue, and alpha).

For example, nc.textnode.set_color(textnode_, 1, 1, 0, 1) will make the text yellow.

Alignment

When the string to render has at least one \n character the text will span multiple lines.

In this case you can choose how to align them (left, right, or center) with the set_alignment() function.

For example, nc.textnode.set_alignment(textnode_, nc.text_alignment.CENTER) will align multiple lines at the center.

To set the alignment, we have used again a special constant value. There are three values: nc.text_alignment.LEFT, nc.text_alignment.RIGHT, and nc.text_alignment.CENTER.

Putting it all together

Complete example #02
nc = ncine

function nc.on_pre_init(cfg)
	cfg.resolution = {x = 1280, y = 720}
	cfg.window_title = "nCine example"
	cfg.console_log_level = nc.log_level.INFO
	return cfg
end

function nc.on_init()
	local rootnode = nc.application.get_rootnode()

	font_ = nc.font.new(nc.fs.get_data_path().."fonts/DroidSans32_256.fnt",
	                    nc.fs.get_data_path().."fonts/DroidSans32_256.png")

	textnode_ = nc.textnode.new(rootnode, font_, 256)
	nc.textnode.set_color(textnode_, 1, 1, 0, 1)
	nc.textnode.set_alignment(textnode_, nc.text_alignment.LEFT)
end

function nc.on_frame_start()
	local str = string.format("FPS: %.2f\nFrame time: %.2f ms", 1.0 / nc.application.get_interval(), 1000 * nc.application.get_interval())
	nc.textnode.set_string(textnode_, str)

	nc.textnode.set_position(textnode_, nc.textnode.get_width(textnode_) * 0.5,
	                                    nc.application.get_height() - nc.textnode.get_height(textnode_) * 0.5)
end

function nc.on_shutdown()
	nc.textnode.delete(textnode_)
	nc.font.delete(font_)
end

Tutorial #03 - Audio

Audio buffer and player

To play sounds and music you have to first load the audio data and then associate it with a player. The concept is similar to textures and sprites, or fonts and text nodes.

In this case the two objects are audio_buffer and audio_buffer_player.

The audio_buffer.new() function takes the path to a WAV or OGG Vorbis file, while the audio_buffer_player.new() function takes the audio buffer object you have just created.

Once the player has been created, you can use the play(), pause(), and stop() functions to control it.

nc = ncine

function nc.on_init()
	audio_buffer_ = nc.audio_buffer.new(nc.fs.get_data_path().."sounds/coins.ogg")
	player_ = nc.audio_buffer_player.new(audio_buffer_)
end

function nc.on_mouse_button_released(event)
	nc.audio_buffer_player.stop(player_)
	nc.audio_buffer_player.play(player_)
end

function nc.on_shutdown()
	nc.audio_buffer_player.delete(player_)
	nc.audio_buffer.delete(audio_buffer_)
end

Music and stream players

When you have a big music file you don’t usually want to load all of it in memory.

It is preferable to stream it: to load and decode only the small part that is currently being played.

You can achieve this by using an audio_stream_player. It can be controlled with the same play(), pause(), and stop() functions.

To create an audio_stream_player you don’t need an audio_buffer, you just pass the file to load, like this: nc.audio_stream_player.new(nc.fs.get_data_path().."sounds/music.ogg").

Audio player properties

You can set some properties for both type of players, like the gain (volume), the pitch, or the looping state.

	nc.audio_buffer_player.set_gain(audio_buffer_, 1.0)
	nc.audio_buffer_player.set_pitch(audio_buffer_, 0.5)
	nc.audio_stream_player.set_looping(stream_player_, true)

You can query those properties with get_gain(), get_pitch(), and is_looping().

You can also query the state of the player with functions like is_playing(), is_paused(), or is_stopped().

Putting it all together

Complete example #03
nc = ncine

function nc.on_init()
	audio_buffer_ = nc.audio_buffer.new(nc.fs.get_data_path().."sounds/coins.ogg")
	player_ = nc.audio_buffer_player.new(audio_buffer_)
end

function nc.on_shutdown()
	nc.audio_buffer_player.delete(player_)
	nc.audio_buffer.delete(audio_buffer_)
end

function nc.on_mouse_button_released(event)
	local looping = nc.audio_buffer_player.is_looping(player_)

	if event.button == nc.mouse_button.LEFT then
		if looping == false then
			nc.audio_buffer_player.stop(player_)
			nc.audio_buffer_player.play(player_)
		else
			local playing = nc.audio_buffer_player.is_playing(player_)
			if playing then
				nc.audio_buffer_player.pause(player_)
			else
				nc.audio_buffer_player.play(player_)
			end
		end
	elseif event.button == nc.mouse_button.RIGHT then
		nc.audio_buffer_player.set_looping(player_, not looping)
	end
end

Tutorial #04 - Animated sprites

Spritesheets and rectangles

For this tutorial we are going to learn how to create an animated sprite using a spritesheet with separated walk frames.

First of all we need to load the spritesheet texture. There is nothing special about a texture that contains more than one image, we load it as usual.

The difference is in how the sprite (regular or animated) is going to use only a portion of that texture, by definiing a rectangle.

When an nCine method needs a rectangle, it refers to a table that defines a top-left corner, a width, and a height: rect = {x = 0, y = 0, w = 128, h = 128}.

The concept of a bigger texture containing more sub-images (being frames of an animation or just different sprites) can also be called a texture atlas.

Creating an animated sprite

The following snippet of code loads a textures and assigns it to an animated sprite. Then it defines an animation, it adds it to the animated sprite, and make it active. The last step is removing the pause to see the sprite animate.

nc = ncine

function nc.on_init()
	spritesheet_ = nc.texture.new(nc.fs.get_data_path().."textures/spritesheet.png")

	local resolution = nc.application.get_resolution()
	anim_sprite_ = nc.animated_sprite.new(nc.application.get_rootnode(), spritesheet_, resolution.x * 0.5, resolution.y * 0.5)

	local animation = { frame_duration = 0.12, loop_mode = nc.loop_mode.ENABLED, rewind_mode = nc.rewind_mode.FROM_START, rect_size = { x = 48, y = 48 }, source_rect = { x = 0, y = 0, w = 144, h = 96 } }
	nc.animated_sprite.add_animation(anim_sprite_, animation)

	nc.animated_sprite.set_animation_index(anim_sprite_, 0)
	nc.animated_sprite.set_paused(anim_sprite_, false)
end

function nc.on_shutdown()
	nc.animated_sprite.delete(anim_sprite_)
	nc.texture.delete(spritesheet_)
end

But how did we define an animation? We created a table with various keys.

  • The frame_duration key specifies how many seconds should a frame stay on screen before the next one will be displayed.

  • The loop_mode key is used to define if the animation will continue playing after it reaches the last frame.

  • It can have two values: nc.loop_mode.ENABLED or nc.loop_mode.DISABLED.

  • The rewind_mode key is used to define what happens when a looping animation reaches the last frame. Should it start again from the first one or go backward until the first one before playing again normally?

  • It can have two values: nc.rewind_mode.FROM_START or nc.rewind_mode.BACKWARD.

  • The rect_size is a two component vector (like the return value of nc.application.get_resolution(), for example) that specifies the size of a frame

  • The source_rect is a rectangle area inside the texture that will be used to create frames. The frames will all have the size you specified earlier. They will start at the corner defined by the x and y components, and proceeds from left to right and from top to bottom, within the bounds defined by the x + w and y + h.

There are also two more optional keys that you can use to achieve more flexibility.

  • The num_rectangles_to_skip key is a number that specifies how many frames should be skipped before adding rectangles from the source_rect area.

  • The padding key is a two component vector that defines the horizontal and vertical distance between adjacent rectangles.

Defining a list of rectangles

There is an alternative way of defining the rectangles that compose an animation. It can be used when the automatic way shown in the previous paragraph is not flexible enough for your needs.

As before, you will provide a value for the frame_duration, loop_mode, and rewind_mode keys. But after those you can list the rectangles you need for your animation.

local animation = {
	frame_duration = 0.12, loop_mode = nc.loop_mode.ENABLED, rewind_mode = nc.rewind_mode.FROM_START,
	{x = 0, y = 0, w = 48, h = 48},
	{x = 48, y = 0, w = 48, h = 48},
	{x = 96, y = 0, w = 48, h = 48},
	{x = 144, y = 0, w = 48, h = 48},
	{x = 0, y = 48, w = 48, h = 48},
	{x = 48, y = 48, w = 48, h = 48},
	{x = 96, y = 48, w = 48, h = 48},
	{x = 144, y = 48, w = 48, h = 48},
}

Additional animated sprite functions

There are some more useful functions that you can use with animated_sprite objets.

If you add more than one animation you can query the number with num_animations then, as you have seen earlier, you can set the active one with set_animation_index(), or query it with get_animation_index(). You can then clear all animations with clear_animations().

You can play or pause the animation by using set_paused() and query its state with is_paused().

You can also handle the animation yourself by changing the frame with set_frame(), and querying the active one with get_frame().

Putting it all together

Complete example #04
nc = ncine

function nc.on_init()
	spritesheet_ = nc.texture.new(nc.fs.get_data_path().."textures/spritesheet.png")

	local resolution = nc.application.get_resolution()
	anim_sprite_ = nc.animated_sprite.new(nc.application.get_rootnode(), spritesheet_, resolution.x * 0.5, resolution.y * 0.5)

	local animation = { frame_duration = 0.12, loop_mode = nc.loop_mode.ENABLED, rewind_mode = nc.rewind_mode.FROM_START, rect_size = { x = 48, y = 48 }, source_rect = { x = 0, y = 0, w = 144, h = 96 } }
	nc.animated_sprite.add_animation(anim_sprite_, animation)

	nc.animated_sprite.set_animation_index(anim_sprite_, 0)
	nc.animated_sprite.set_paused(anim_sprite_, false)
end

function nc.on_shutdown()
	nc.animated_sprite.delete(anim_sprite_)
	nc.texture.delete(spritesheet_)
end

function nc.on_mouse_button_released(event)
	if event.button == nc.mouse_button.LEFT then
		local is_paused = nc.animated_sprite.is_paused(anim_sprite_)
		nc.animated_sprite.set_paused(anim_sprite_, not is_paused)
	elseif event.button == nc.mouse_button.RIGHT then
		nc.animated_sprite.set_paused(anim_sprite_, true)

		local num_frames = nc.animated_sprite.num_frames(anim_sprite_)
		local frame_index = nc.animated_sprite.get_frame(anim_sprite_)
		frame_index = (frame_index + 1) % num_frames

		nc.animated_sprite.set_frame(anim_sprite_, frame_index)
	end
end

Tutorial #05 - Keyboard and gamepad

Keyboard input

In this tutorial we are going to learn more about user inputs.

The are two ways of looking at inputs: you can query their current state or you can listen for events (a change in state).

The keyboard inputs work just the same. You can query for their state (which keys are pressed in this frame) or you can react to events (the user pressing or releasing a key).

Let’s build an example using the animated sprite from last tutorial.

nc = ncine

function nc.on_frame_start()
	local pos = nc.animated_sprite.get_position(anim_sprite_)
	local step = 100 * nc.application.get_interval()
	local moved = false

	local key_state = nc.input.key_state()
	if nc.input.key_down(key_state, nc.keysym.UP) or nc.input.key_down(key_state, nc.keysym.W) then
		pos.y = pos.y + step
		nc.animated_sprite.set_rotation(anim_sprite_, 180)
		moved = true
	elseif nc.input.key_down(key_state, nc.keysym.DOWN) or nc.input.key_down(key_state, nc.keysym.S) then
		pos.y = pos.y - step
		nc.animated_sprite.set_rotation(anim_sprite_, 0)
		moved = true
	elseif nc.input.key_down(key_state, nc.keysym.LEFT) or nc.input.key_down(key_state, nc.keysym.A) then
		pos.x = pos.x - step
		nc.animated_sprite.set_rotation(anim_sprite_, 270)
		moved = true
	elseif nc.input.key_down(key_state, nc.keysym.RIGHT) or nc.input.key_down(key_state, nc.keysym.D) then
		pos.x = pos.x + step
		nc.animated_sprite.set_rotation(anim_sprite_, 90)
		moved = true
	end

	nc.animated_sprite.set_paused(anim_sprite_, not moved)
	if moved then
		nc.animated_sprite.set_position(anim_sprite_, pos)
	else
		nc.animated_sprite.set_frame(anim_sprite_, 0)
	end
end

function nc.on_key_released(event)
	if event.sym == nc.keysym.ESCAPE then
		nc.application.quit()
	end
end

In this example we are using the keyboard state to move the animated sprite.

We first get the current position on screen and calculate a movement based on the last frame time, then we check which key is pressed this frame.

The current keyboard state is retrieved using input.key_state(). We then use input.key_down() on the state object to query if a specific key is pressed. We check both arrows and WASD keys to offer the user two ways of moving our animated sprite.

If the user pressed one of these keys we update the position vector and change the sprite rotation with set_rotation().

We also set the value of the moved flag that will later be used to play/pause the animation, set the sprite position, and reset the animation frame.

The snippet also shows the use of the on_key_released() event callback. The event table received by the function contains a sym key, its value can be checked against the keysym table just like we did before with the keyboard state.

As you might expect, there is also a mirror on_key_pressed() event callback that is called when the user press a key.

Gamepad input

Mapping

The gamepad input works in a similar way: you can query its state or listen for events.

You need to know that there are two different "modes" when it comes to the gamepad: the raw state adn events and the mapped ones.

When you connect a gamepad, the system will assign a number id to its axes and buttons, but you will not know which button is the "Start" button, for example.

You can, of course, ask the user to assign known values by telling him or her to press a button or move an axis when you are ready to detect a specific button or axis.

This work is called mapping, and fortunately it has already been done for a long list of devices. The nCine uses the SDL mapping database.

The mapping uses the XBox gamepad as a model and assigns each axis and button to this layout.

Mapped events

If your gamepad is found in the mapping database, you can use the mapped state and events.

function nc.on_joymapped_axis_moved(event)
	local pos = nc.animated_sprite.get_position(anim_sprite_)
	local step = 100 * nc.application.get_interval()
	local moved = false

	if event.axis == nc.joy_axis.LY then
		pos.y = pos.y + step * event.value
		if event.value > 0 then
			nc.animated_sprite.set_rotation(anim_sprite_, 180)
			moved = true
		elseif event.value < 0 then
			nc.animated_sprite.set_rotation(anim_sprite_, 0)
			mnvoed = true
		end
	elseif event.axis == nc.joy_axis.LX then
		pos.x = pos.x + step * event.value
		if event.value > 0 then
			nc.animated_sprite.set_rotation(anim_sprite_, 270)
			moved = true
		elseif event.value < 0 then
			nc.animated_sprite.set_rotation(anim_sprite_, 90)
			moved = true
		end
	end

	nc.animated_sprite.set_paused(anim_sprite_, not moved)
	if moved then
		nc.animated_sprite.set_position(anim_sprite_, pos)
	else
		nc.animated_sprite.set_frame(anim_sprite_, 0)
	end
end

function nc.on_joymapped_button_released(event)
	if event.button == nc.joy_button.GUIDE then
		nc.application.quit()
	end
end

The code is similar to the one we used to handle keyboard input, but we are using events this time.

The on_joymapped_axis_moved() callback receives an event table that has an axis and a value key, by querying them you will now which axis has been moved and its new position.

The LY and LX axes are the vertical and horizontal direction of the left stick. But you can also use RY and RX if you want to use the right stick. The two remaining axes in the mapping model are LTRIGGER and RTRIGGER for the two analog triggers (also called LT or L2, and RT and R2).

Just as we did with the keyboard, we are also listening to the on_joymapped_button_released() event to quit the application when the user preses the GUIDE button. There is, of course, also a on_joymapped_button_pressed() callback, as well as a list of buttons you can react to.

The available mapped buttons are:

  • A, B, X, Y

  • BACK, GUIDE, START

  • LSTICK, RSTICK

  • LBUMPER, RBUMPER

  • DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT,

Putting it all together

Complete example #05
nc = ncine

function nc.on_init()
	spritesheet_ = nc.texture.new(nc.fs.get_data_path().."textures/spritesheet.png")

	local resolution = nc.application.get_resolution()
	anim_sprite_ = nc.animated_sprite.new(nc.application.get_rootnode(), spritesheet_, resolution.x * 0.5, resolution.y * 0.5)

	local animation = { frame_duration = 0.12, loop_mode = nc.loop_mode.ENABLED, rewind_mode = nc.rewind_mode.FROM_START, rect_size = { x = 48, y = 48 }, source_rect = { x = 0, y = 0, w = 144, h = 96 } }
	nc.animated_sprite.add_animation(anim_sprite_, animation)

	nc.animated_sprite.set_animation_index(anim_sprite_, 0)
	nc.animated_sprite.set_paused(anim_sprite_, false)
end

function nc.on_frame_start()
	local pos = nc.animated_sprite.get_position(anim_sprite_)
	local step = 100 * nc.application.get_interval()
	local moved = false

	local key_state = nc.input.key_state()
	if nc.input.key_down(key_state, nc.keysym.UP) or nc.input.key_down(key_state, nc.keysym.W) then
		pos.y = pos.y + step
		nc.animated_sprite.set_rotation(anim_sprite_, 180)
		moved = true
	elseif nc.input.key_down(key_state, nc.keysym.DOWN) or nc.input.key_down(key_state, nc.keysym.S) then
		pos.y = pos.y - step
		nc.animated_sprite.set_rotation(anim_sprite_, 0)
		moved = true
	elseif nc.input.key_down(key_state, nc.keysym.LEFT) or nc.input.key_down(key_state, nc.keysym.A) then
		pos.x = pos.x - step
		nc.animated_sprite.set_rotation(anim_sprite_, 270)
		moved = true
	elseif nc.input.key_down(key_state, nc.keysym.RIGHT) or nc.input.key_down(key_state, nc.keysym.D) then
		pos.x = pos.x + step
		nc.animated_sprite.set_rotation(anim_sprite_, 90)
		moved = true
	end

	nc.animated_sprite.set_paused(anim_sprite_, not moved)
	if moved then
		nc.animated_sprite.set_position(anim_sprite_, pos)
	else
		nc.animated_sprite.set_frame(anim_sprite_, 0)
	end
end

function nc.on_shutdown()
	nc.animated_sprite.delete(anim_sprite_)
	nc.texture.delete(spritesheet_)
end

function nc.on_key_released(event)
	if event.sym == nc.keysym.ESCAPE then
		nc.application.quit()
	end
end

function nc.on_joymapped_axis_moved(event)
	local pos = nc.animated_sprite.get_position(anim_sprite_)
	local step = 100 * nc.application.get_interval()
	local moved = false

	if event.axis == nc.joy_axis.LY then
		pos.y = pos.y + step * event.value
		if event.value > 0 then
			nc.animated_sprite.set_rotation(anim_sprite_, 180)
			moved = true
		elseif event.value < 0 then
			nc.animated_sprite.set_rotation(anim_sprite_, 0)
			mnvoed = true
		end
	elseif event.axis == nc.joy_axis.LX then
		pos.x = pos.x + step * event.value
		if event.value > 0 then
			nc.animated_sprite.set_rotation(anim_sprite_, 270)
			moved = true
		elseif event.value < 0 then
			nc.animated_sprite.set_rotation(anim_sprite_, 90)
			moved = true
		end
	end

	nc.animated_sprite.set_paused(anim_sprite_, not moved)
	if moved then
		nc.animated_sprite.set_position(anim_sprite_, pos)
	else
		nc.animated_sprite.set_frame(anim_sprite_, 0)
	end
end

function nc.on_joymapped_button_released(event)
	if event.button == nc.joy_button.GUIDE then
		nc.application.quit()
	end
end

Tutorial #06 - The Scenegraph

The transformations and the scenegraph

Until now we have always added our sprites to the root node of the scenegraph, without really explaining why or what the scenegraph is.

Each regular sprite, animated sprite, or text node is a scene node that can be added to the scenegraph in order to be rendered on screen.

The scenegraph is a tree that starts at the root node and that can have many branches (or children). Each node can then have children on its own.

Having a node that is a child of another one is useful to carry the parent transformations while transforming it independently.

Think of the Earth and the Moon. The Moon revolves around the Earth, but the Earth revolves around the Sun. It is a lot easier to model those two movements independently instead of thinking about how the Moon revolves around the Sun.

nc = ncine

function nc.on_init()
	texture_ = nc.texture.new(nc.fs.get_data_path().."textures/texture4.png")

	local resolution = nc.application.get_resolution()
	local pos = { x = resolution.x * 0.5, y = resolution.y * 0.5 }
	sun_sprite_ = nc.sprite.new(nc.application.get_rootnode(), texture_, pos)

	earth_sprite_ = nc.sprite.new(sun_sprite_, texture_, resolution.x * 0.25, 0)
	nc.sprite.set_scale(earth_sprite_, 0.5)

	moon_sprite_ = nc.sprite.new(earth_sprite_, texture_, resolution.x * 0.1, 0)
	nc.sprite.set_scale(moon_sprite_, 0.5)

	angle_ = 0.0
end

function nc.on_frame_start()
	angle_ = angle_ + 50 * nc.application.get_interval()

	nc.sprite.set_rotation(sun_sprite_, angle_ * 0.5)
	nc.sprite.set_rotation(earth_sprite_, angle_)
	nc.sprite.set_rotation(moon_sprite_, angle_)
end

The second parameter of the sprite.new() function is the parent node of the sprite we are going to create.

Only the Sun is a child of the root node. Then the Earth is a child of the Sun and the Moon is a child of Earth.

With this setup it’s easy to model their revolution by just rotating them.

We have also scaled down the sprites to make their relation clearer. Both the Earth and the Moon have been scaled to half their original sizes, but the Moon inherits its parent transformation and ends up being scaled to a fourth of its original size.

Modifying the scenegraph

Assigning a parent when creating a new node is not the only way of changing the relationships inside the scenegraph.

There are many other functions that you can use to query or modify the associations between nodes.

If you want to know how many children does a parent node have you can use num_children(). You can then retrieve them by providing an index to get_child().

You can also use the get_children() function and retrieve a list of all children in one go.

To retrieve the parent of a node use the get_parent() function, or set one with set_parent().

You can use add_child() and remove_child() to add or remove a specific node object from a parent, or use remove_child_at() if you know the index.

If you want to remove all children from a node it is faster to call remove_all_children() than to cycle over them.

Putting it all together

Complete example #06
nc = ncine

function nc.on_init()
	texture_ = nc.texture.new(nc.fs.get_data_path().."textures/texture4.png")

	local resolution = nc.application.get_resolution()
	local pos = { x = resolution.x * 0.5, y = resolution.y * 0.5 }
	sun_sprite_ = nc.sprite.new(nc.application.get_rootnode(), texture_, pos)

	earth_sprite_ = nc.sprite.new(sun_sprite_, texture_, resolution.x * 0.25, 0)
	nc.sprite.set_scale(earth_sprite_, 0.5)

	moon_sprite_ = nc.sprite.new(earth_sprite_, texture_, resolution.x * 0.1, 0)
	nc.sprite.set_scale(moon_sprite_, 0.5)

	angle_ = 0.0
end

function nc.on_frame_start()
	angle_ = angle_ + 50 * nc.application.get_interval()

	nc.sprite.set_rotation(sun_sprite_, angle_ * 0.5)
	nc.sprite.set_rotation(earth_sprite_, angle_)
	nc.sprite.set_rotation(moon_sprite_, angle_)
end

function nc.on_shutdown()
	nc.sprite.delete(moon_sprite_)
	nc.sprite.delete(earth_sprite_)
	nc.sprite.delete(sun_sprite_)
	nc.texture.delete(texture_)
end

function nc.on_mouse_button_released(event)
	if event.button == nc.mouse_button.LEFT then
		if nc.sprite.get_parent(moon_sprite_) == earth_sprite_ then
			nc.sprite.set_parent(moon_sprite_, sun_sprite_)
		elseif nc.sprite.get_parent(moon_sprite_) == sun_sprite_ then
			nc.sprite.set_parent(moon_sprite_, nc.application.get_rootnode())
		elseif nc.sprite.get_parent(moon_sprite_) == nc.application.get_rootnode() then
			nc.sprite.set_parent(moon_sprite_, earth_sprite_)
		end
	elseif event.button == nc.mouse_button.RIGHT then
		local sun_children = nc.sprite.get_children(sun_sprite_)
		if #sun_children > 1 or nc.sprite.num_children(earth_sprite_) == 0 then
			nc.sprite.add_child(earth_sprite_, moon_sprite_)
		end
	end
end

Tutorial #07 - Mesh sprites

Uses of a mesh sprite

We have already discussed regular and animated sprites, but we left out mesh ones.

While regular and animated sprites are rendered as a rectangle made of two triangles, mesh sprites are based on an arbitrary mesh defined by the user.

You can define your own set of positions and UV coordinates and thus the shape of the sprite. While a regular rectangular sprite can still display any shape on screen by employing transparency, having a mesh define this shape can have better performance on some platforms.

For example, instead a regular sprite with transparency, you can use two mesh sprites. One will cover the non-transparent part of your original sprite, while the second one will only cover the transparent part you might have along the border. This way, you can completely disable transparency while rendering the first one by using set_blending_enabled(false) and possibly achieve some performance gain.

There is also another, maybe more interesting, use for mesh sprites: animating the position of its vertices.

Creating a mesh sprite

In the following snippet, we create a simple sprite with a rectangular mesh. There are various ways to define the vertices, but to keep things simple we use create_vertices_from_texels().

This function gets a list of coordinates in texture space (i.e. between 0 and the dimension of the texture in pixels) and creates a mesh sprite with the correct positions and UV coordinates.

nc = ncine

function nc.on_init()
	texture_ = nc.texture.new(nc.fs.get_data_path().."textures/texture2.png")

	local resolution = nc.application.get_resolution()
	mesh_sprite_ = nc.mesh_sprite.new(nc.application.get_rootnode(), texture_, resolution.x * 0.5, resolution.y * 0.5)

	nc.mesh_sprite.create_vertices_from_texels(
		mesh_sprite_, {{x = 0, y = 0}, {x = 128, y = 0}, {x = 0, y = 128}, {x = 128, y = 128}}, nc.texture_cut_mode.RESIZE
	)
end

You should also provide a cut mode (either nc.texture_cut_mode.RESIZE or nc.texture_cut_mode.CROP) to select how the algorithm will cut the texture with the edges you define.

In this case, we are just encompassing the whole texture (whose dimensions are 128x128) with a rectangle by specifying four vectors that span the entire surface.

Defining a list of vertices

Let’s now see a different way of defining our vertices, that will later help animate them.

	local vertices = {
		{x = 0.5, y = 0.5, u = 0.5, v = 0.5},
		{x = 0.0, y = 0.0, u = 0.0, v = 1.0},
		{x = 1.0, y = 0.0, u = 1.0, v = 1.0},
		{x = 1.0, y = 0.0, u = 1.0, v = 1.0},

		{x = 0.5, y = 0.5, u = 0.5, v = 0.5},
		{x = 1.0, y = 0.0, u = 1.0, v = 1.0},
		{x = 1.0, y = 1.0, u = 1.0, v = 0.0},
		{x = 1.0, y = 1.0, u = 1.0, v = 0.0},

		{x = 0.5, y = 0.5, u = 0.5, v = 0.5},
		{x = 1.0, y = 1.0, u = 1.0, v = 0.0},
		{x = 0.0, y = 1.0, u = 0.0, v = 0.0},
		{x = 0.0, y = 1.0, u = 0.0, v = 0.0},

		{x = 0.5, y = 0.5, u = 0.5, v = 0.5},
		{x = 0.0, y = 1.0, u = 0.0, v = 0.0},
		{x = 0.0, y = 0.0, u = 0.0, v = 1.0},
		{x = 0.0, y = 0.0, u = 0.0, v = 1.0},
	}

	nc.mesh_sprite.copy_vertices(mesh_sprite_, vertices)

We are defining a list of vertices by specifying the positions and UV coordinates (both going from 0 to 1 to span the whole texture space) and using the copy_vertices() function. Just keep in mind that the v coordinate goes in the opposite direction, so if y = 0.0 then v = 1.0 and vice versa.

As you can notice, the list defines four triangles but repeats the last vertex. The repeated one is called a degenerate vertex and is needed to achieve our topology: a rectangle with a shared center vertex.

Mesh sprites use GL_TRIANGLE_STRIP primitives, where each new triangle is defined by a vertex and the two preceding it. In this case, it would have come in handy to have GL_TRIANGLE_FAN primitives but there is currently no way to change this.

We could have also equivalently used the create_vertices_from_texels() function.

	local texels = {
		{x = 64,  y = 64 },
		{x = 0,   y = 0  },
		{x = 128, y = 0  },
		{x = 128, y = 0  },

		{x = 64,  y = 64 },
		{x = 128, y = 0  },
		{x = 128, y = 128},
		{x = 128, y = 128},

		{x = 64 , y = 64 },
		{x = 128, y = 128},
		{x = 0  , y = 128},
		{x = 0  , y = 128},

		{x = 64 , y = 64 },
		{x = 0  , y = 128},
		{x = 0  , y = 0  },
		{x = 0  , y = 0 },
	}

	nc.mesh_sprite.create_vertices_from_texels(mesh_sprite_, texels, nc.texture_cut_mode.RESIZE)

Animating the vertices

And now for the fun part: animating the vertices!

But first let’s define the vertices in yet another way, the fastest one.

	vertices_ = {
		 0.0, 0.0, 0.5, 0.5,
		-0.5, -0.5, 0.0, 1.0,
		 0.5, -0.5, 1.0, 1.0,
		 0.5, -0.5, 1.0, 1.0,

		 0.0,  0.0, 0.5, 0.5,
		 0.5, -0.5, 1.0, 1.0,
		 0.5,  0.5, 1.0, 0.0,
		 0.5,  0.5, 1.0, 0.0,

		 0.0, 0.0, 0.5, 0.5,
		 0.5, 0.5, 1.0, 0.0,
		-0.5, 0.5, 0.0, 0.0,
		-0.5, 0.5, 0.0, 0.0,

		 0.0,  0.0, 0.5, 0.5,
		-0.5,  0.5, 0.0, 0.0,
		-0.5, -0.5, 0.0, 1.0,
		-0.5, -0.5, 0.0, 1.0,
	}

	nc.mesh_sprite.emplace_vertices(mesh_sprite_, vertices_)

	sprite_vertices_ = nc.mesh_sprite.get_vertices(mesh_sprite_)

By using emplace_vertices() we spare an internal copy. Besides, the list is now a table of numbers instead of a table of tables, making its parsing even faster.

We have also moved the center of the rectangle to the origin by subtracting 0.5 to both its x and y coordinates.

You can notice the non-local vertices_ and sprite_vertices_ variables, they will both be used in the on_frame_start() function.

function nc.on_frame_start()
	angle_ = angle_ + nc.application.get_interval()

	for i = 5, #sprite_vertices_ do
		local mod = (i - 1) % 4
		if mod == 0 then
			sprite_vertices_[i] = vertices_[i] * (0.25 + math.sin(angle_) * math.sin(angle_) * 0.75)
		elseif mod == 1 then
			sprite_vertices_[i] = vertices_[i] * (0.25 + math.cos(angle_) * math.cos(angle_) * 0.75)
		end
	end

	nc.mesh_sprite.emplace_vertices(mesh_sprite_, sprite_vertices_)
end

We are using again an angle_ parameter to animate with trigonometric functions.

The for loop iterates over all sprite_vertices_ (skipping the first one) and alters them by applying a function to the original vertices_ list. In this case, only the x and y coordinates will be modified, thanks to a modulo division that selects the right components.

We then use the emplace_vertices() function to update their values.

Putting it all together

Complete example #07
nc = ncine

function nc.on_init()
	texture_ = nc.texture.new(nc.fs.get_data_path().."textures/texture2.png")

	local resolution = nc.application.get_resolution()
	mesh_sprite_ = nc.mesh_sprite.new(nc.application.get_rootnode(), texture_, resolution.x * 0.5, resolution.y * 0.5)

	vertices_ = {
		 0.0, 0.0, 0.5, 0.5,
		-0.5, -0.5, 0.0, 1.0,
		 0.5, -0.5, 1.0, 1.0,
		 0.5, -0.5, 1.0, 1.0,

		 0.0,  0.0, 0.5, 0.5,
		 0.5, -0.5, 1.0, 1.0,
		 0.5,  0.5, 1.0, 0.0,
		 0.5,  0.5, 1.0, 0.0,

		 0.0, 0.0, 0.5, 0.5,
		 0.5, 0.5, 1.0, 0.0,
		-0.5, 0.5, 0.0, 0.0,
		-0.5, 0.5, 0.0, 0.0,

		 0.0,  0.0, 0.5, 0.5,
		-0.5,  0.5, 0.0, 0.0,
		-0.5, -0.5, 0.0, 1.0,
		-0.5, -0.5, 0.0, 1.0,
	}

	nc.mesh_sprite.emplace_vertices(mesh_sprite_, vertices_)

	sprite_vertices_ = nc.mesh_sprite.get_vertices(mesh_sprite_)

	angle_ = 0
end

function nc.on_frame_start()
	angle_ = angle_ + nc.application.get_interval()

	for i = 5, #sprite_vertices_ do
		local mod = (i - 1) % 4
		if mod == 0 then
			sprite_vertices_[i] = vertices_[i] * (0.25 + math.sin(angle_) * math.sin(angle_) * 0.75)
		elseif mod == 1 then
			sprite_vertices_[i] = vertices_[i] * (0.25 + math.cos(angle_) * math.cos(angle_) * 0.75)
		end
	end

	nc.mesh_sprite.emplace_vertices(mesh_sprite_, sprite_vertices_)
end

function nc.on_shutdown()
	nc.mesh_sprite.delete(mesh_sprite_)
	nc.texture.delete(texture_)
end