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()
.
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
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
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 audiobuffer
and audiobuffer_player
.
The audiobuffer.new()
function takes the path to a WAV or OGG Vorbis file, while the audiobuffer_player.new()
function takes the audiobuffer 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()
audiobuffer_ = nc.audiobuffer.new(nc.fs.get_data_path().."sounds/coins.ogg")
player_ = nc.audiobuffer_player.new(audiobuffer_)
end
function nc.on_mouse_button_released(event)
nc.audiobuffer_player.stop(player_)
nc.audiobuffer_player.play(player_)
end
function nc.on_shutdown()
nc.audiobuffer_player.delete(player_)
nc.audiobuffer.delete(audiobuffer_)
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 audiostream_player
. It can be controlled with the same play()
, pause()
, and stop()
functions.
To create an audiostream_player
you don’t need an audiobuffer
, you just pass the file to load, like this: nc.audiostream_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.audiobuffer_player.set_gain(audiobuffer_, 1.0)
nc.audiobuffer_player.set_pitch(audiobuffer_, 0.5)
nc.audiostream_player.set_looping(streamplayer_, 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
nc = ncine
function nc.on_init()
audiobuffer_ = nc.audiobuffer.new(nc.fs.get_data_path().."sounds/coins.ogg")
player_ = nc.audiobuffer_player.new(audiobuffer_)
end
function nc.on_shutdown()
nc.audiobuffer_player.delete(player_)
nc.audiobuffer.delete(audiobuffer_)
end
function nc.on_mouse_button_released(event)
local looping = nc.audiobuffer_player.is_looping(player_)
if event.button == nc.mouse_button.LEFT then
if looping == false then
nc.audiobuffer_player.stop(player_)
nc.audiobuffer_player.play(player_)
else
local playing = nc.audiobuffer_player.is_playing(player_)
if playing then
nc.audiobuffer_player.pause(player_)
else
nc.audiobuffer_player.play(player_)
end
end
elseif event.button == nc.mouse_button.RIGHT then
nc.audiobuffer_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
ornc.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
ornc.rewind_mode.BACKWARD
. -
The
rect_size
is a two component vector (like the return value ofnc.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 thex
andy
components, and proceeds from left to right and from top to bottom, within the bounds defined by thex + w
andy + 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 thesource_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
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
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
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
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