Lua Player Tutorial

Prerequisites

You should know the basics of the programming language Lua, there are some good general Lua tutorials at http://lua-users.org/wiki/LuaTutorial.

Very useful for own experiments in pure Lua is LuaIDE, where you can enter some program and run it step-by-step, watching how your variables changes. Another way is to download the standlone version of Lua (lua.exe on Windows), open a command prompt and start lua.exe, where you can enter Lua expressions, like for i=1,10 do io.write(i .. “\n”) end.

Hello World

First install Lua Player like described in the README. Now let’s start with a simple script:

-- create a new Color object
green = Color.new(0, 255, 0)
 
-- show some text on offscreen
screen:print(200, 100, "Hello World!", green)
 
-- flip visible and offscreen
screen.flip()
 
-- wait forevever
while true do
	screen.waitVblankStart()
end

Save this as the file script.lua in the same directory on your PSP, where EBOOT.PBP is. When you start Lua Player, you should see this picture:

The graphics resolution of the PSP is 480 pixels width and 272 pixels height. “Color.new” creates a new color object. The arguments are red, green, blue and alpha (optional), which can range from 0 for off and 255 for full intensity. This is known as the RGB color model. “screen:print” draws some text on the screen, where the first two arguments are the x and y coordinates of the text, then the text follows and optional a color (black is default). x starts on the left side and y on to top side of the display.

There are two screen buffers: one offscreen buffer and one visible screen buffer. All drawing functions goes to the offscreen buffer. This means your print is not visible until you call screen.flip(), which exchanges the offscreen buffer and the visisble screen buffer. This is known as double-buffering. It is implemented as page-flipping (see the wikipedia entry for an explanation), this is the reason for the name “flip”.

Finnaly the while-loop calls waitVblankStart in an endless loop. If you don’t write something like this at the end, your script finishes and you don’t see the result of your drawing, because if started from Lowser, the Lowser GUI will be displayed and if started as a standalone script the restart question will be displayed. If you don’t write the wait function, but use an empty loop, it will generate much CPU usage, because the wait function gives the kernel the possibility to sleep, until the next vertical retrace blank starts.

Animation

Understanding how the pixels in memory are displayed on the screen is important for writing games. The concept of many displays, including the one for the PSP, are the same like for the old cathode ray tube: A beam starts at top left and scans all lines top down. At the bottom it needs some time for returning to the top left, which is called the vertical blank (vblank), because the beam is deactivated while moving back to it’s start position. Of course, in PSP there is no real beam, but you can think of it as if it works like this. With “screen.waitVblankStart()” the script waits for the start of this vblank. While the vblank time no pixels are written to the display, which gives you the time to flip the screen and offscreen to avoid flickering.

Let’s see how an animation would look like with synchronized page flipping:

System.usbDiskModeActivate()
green = Color.new(0, 255, 0)
time = 0
pi = math.atan(1) * 4
while true do
	screen:clear()
 
	x = math.sin(pi * 2 / 360 * time) * 150 + 192.5
	screen:print(x, 100, "Hello World!", green)
	time = time + 1
	if time >= 360 then
		time = 0
	end
 
	screen.waitVblankStart()
	screen.flip()
 
	pad = Controls.read()
	if pad:start() then
		break
	end
end

Within the while-loop, first the offscreen is cleared. Then some text is drawn and then the script waits for the vblank and flips the visible screen and the offscreen. On PSP the vertical refresh rate is 60 Hz, which means that the text needs 6 seconds to start again from the same position (the sin function has a period of 2*pi, so for a full period “time” needs to go from 0 to 360 and “time” is incremented 60 times per second, which means a period takes 6 seconds). Finally the pad is checked for the start-key, which exits the loop.

You can use this code as a start for your own programs. The “System.usbDiskModeActivate()” at start activates the USB mode and the pad-code ends the loop, when you press “start”. The Lua Player program starts the program again, too, if “start” is pressed, which means you have a fast turnaround time while developing: Start your script on PSP the first time, which enables USB, then open the script from your USB drive in a text editor, save your changes and then every click on “start” restarts your changed script immediatly.

Images

First copy this image as “background.png” on your PSP:

and then “smiley.png”:

Now a program for an animation of the smiley:

System.usbDiskModeActivate()
green = Color.new(0, 255, 0)
time = 0
pi = math.atan(1) * 4
background = Image.load("background.png")
smiley = Image.load("smiley.png")
while true do
	screen:blit(0, 0, background, 0, 0, background:width(), background:height(), false)
 
	x = math.sin(pi * 2 / 250 * time) * 200 + 220.5
	y = 172 - math.abs(math.sin(pi * 2 / 125 * time) * 150)
	screen:blit(x, y, smiley)
	time = time + 1
	if time >= 500 then
		time = 0
	end
 
	screen.waitVblankStart()
	screen.flip()
 
	pad = Controls.read()
	if pad:start() then
		break
	end
end

You can see the general structure of our main loop is nearly the same like in the previous example. But instead of calling “screen:clear()”, a “screen:blit” is called, which draws the background. Then a “screen:blit(x, y, smiley)” draws another image on top of the background. There are less arguments, because it uses the default arguments for the blit function, which means that “alpha” is set to “true”. This means, all pixels, which are transparent in the image, are not blitted. For example some graphics programs shows you the image like this:

The checked pattern is transparent and will not be drawn, if blitted with alpha=true in Lua Player.

If you have many objects to draw to screen, it might be faster to use a multi-layer technic: Create an empty image with “Image.createEmpty” (empty images are created by default with all transparent pixels), draw the static parts of your game to this image and for every loop after vblank, first draw the background to screen, then the image with your static parts and finally the dynamic parts. See the Snake game for an example how to do this.

Controls

You can use the controls on your PSP with the Controls class. “Controls.read()” reads the current state of the PSP controls and for example the cross function

on the result returns true, if the cross is pressed and false otherwise. analogX and analogY returns the position of the analog pad. The range is from -128 to 127, but values below 32 can be produced even if the pad is centered. A sample for a drawing program. Use the analog pad for moving the cursor, hold down the cross for drawing, press “select” for a screenshot and “start” for end.

red = Color.new(255, 0, 0);
black = Color.new(0, 0, 0);
white = Color.new(255, 255, 255);
 
canvas = Image.createEmpty(480, 272)
canvas:clear(white)
 
brush = {}
eraser = {}
 
x0 = 0
 
y0 = 0
x1 = 0
y1 = 0
while true do
	pad = Controls.read()
	dx = pad:analogX()
	if math.abs(dx) > 32 then
		x0 = x0 + dx / 64
	end
	dy = pad:analogY()
	if math.abs(dy) > 32 then
		y0 = y0 + dy / 64
	end
	if pad:cross() then
		canvas:drawLine(x0, y0, x1, y1, black)
	end
	x1 = x0
	y1 = y0
	screen:blit(0, 0, canvas, 0, 0, canvas:width(), canvas:height(), false)
	screen:drawLine(x1 - 5, y1, x1 + 5, y1, red)
	screen:drawLine(x1, y1 - 5, x1, y1 + 5, red)
	screen.waitVblankStart()
	screen.flip()
	if pad:start() then break end
	if pad:select() then screen:save("screenshot.tga") end
end

Sample screenshot (I know, I’m not an artist :-)

Instead of the drawLine commands with the red color for the crosshair cursor, you can use a screen:blit(x1, y1, yourCursorImage) for drawing an image with your cursor and you can delete the drawLine on cross pressed, if you don’t want to implement a line drawing program.

You can stop Controls to repeat multiple by adding the following lines to your code

function readControls()
	pad = Controls.read()
	if pad ~= prevPad then
		--yourcodehere
	end
	prevPad = pad
end

...or:

function readControls()
	pad = Controls.read()
	if pad == prevPad then return end
	prevPad = pad
 
	--yourcodehere
end

Sound

Lua Player supports sound in WAV format and music in the MOD formats UNI, IT, XM, S3M, MOD, MTM, STM, DSM, MED, FAR, ULT and 669 (use Google to find some editors for it and perhaps some music files, but ask the composer, if you are allowed to use it).

Playing a music is easy: For example to play the file “stranglehold.xm” (composed by Jeroen Tel, (C) 1995 Maniacs of Noise) from the Snake game (Jeroen has allowed to use it in this game), which is included in the standard distribution with Lua Player, just call “Music.playFile(”stranglehold.xm”, true)”. If you specify “false” instead of “true”, the sound will not loop. Call “Music.stop()” to stop the music.

For WAV files first you load the file like with Images: “explode = Sound.load(”explode.wav”)”. Later you can call “explode:play()” to play the music.

Lowser integration

Just name the script file “index.lua” and place the game directory in the Applications directory in the luaplayer directory on memory stick and make the game exit on pressing “start” (don’t forget to stop music and sound on game exit). See the README files in the standard distribution for more information about how to distribute your game as standalone with EBOOT.PBP or without.

File I/O

File I/O is the standard Lua function. A simple example from the Snake game for loading and saving the highscore:

function saveHighscore()
	file = io.open(highscoreFile, "w")
	if file then
		file:write(high)
		file:close()
	end
end
 
function loadHighscore()
	file = io.open(highscoreFile, "r")
	if file then
		high = file:read("*n")
		file:close()
	end
end

where “high” is a global variable, which holds the current highscore. See the Lua manual for details which parameters you can use for the read function and the Snake game for a more complicated sample, which saves and loads the settings of the game.

Link to File IO Section of the Lua Manual

Full game sample with tiles, animation etc.

Now we’ll write a simple 2D game, where you can edit a map for multiple levels. On C64 there was a charset with 256 characters with 8×8 pixels, which were the bulding blocks of the map. We can save this bulding blocks in one image, also known as a tile set. We’ll use 16×16 pixel tiles. Here is an example for a cave like game or for some game like Pac Man:

The tiles:

Lets number the tiles from a to p:

abcd
efgh
ijkl
mnop

Now we can draw a map with it and display it with some Lua Player code:

System.usbDiskModeActivate()
 
tiles = Image.load("tiles.png")
 
map = {
	"mmmmmmmmmmmmmmmmmmmmmmmmmmmmmm",
	"mmmmmmmmmmmmmmmmmmmmmmmmmmmmmm",
	"mmeaaaaaaaaaaaaaaaaaaaaaaaafmm",
	"mmdpnnnnnnnnnnnnnnnnnnnnnnpbmm",
	"mmdokcccccclnkccccccccccclnbmm",
	"mmdojaaaaafdnbmmmmmmmmmmmdnbmm",
	"mmdooooooobdnbmmmmmmmmmmmdnbmm",
	"mmdokccclobdnbmmmmmmmmmmmdnbmm",
	"mmdobmmmdobdnbmmmmmmmmmmmdnbmm",
	"mmdobmmmdobdnjaaaaaaaaaafdnbmm",
	"mmdobmmmdpbdnnnnnnnnnnnnbdnbmm",
	"mmdobmmmhcghcccccccccclnbdnbmm",
	"mmdojaaaaaaaaaaaaaaaaainjinbmm",
	"mmdpnnnnnnnnnnnnnnnnnnnnnnpbmm",
	"mmhccccccccccccccccccccccccgmm",
	"mmmmmmmmmmmmmmmmmmmmmmmmmmmmmm",
	"mmmmmmmmmmmmmmmmmmmmmmmmmmmmmm"
}
 
function drawTile(tile, x, y)
	local tileX = math.mod(tile, 4)
	local tileY = math.floor(tile / 4)
	screen:blit(16 * x, 16 * y, tiles, 17 * tileX + 1, 17 * tileY + 1, 16, 16, false)
end	
 
while true do
	pad = Controls.read()
	if pad:start() then break end
	screen:clear()
	for y = 1, 17 do
		line = map[y]
		for x = 1, 30 do
			tile = string.byte(line, x) - string.byte("a")
			drawTile(tile, x - 1, y - 1)
		end
	end
	screen.waitVblankStart()
	screen:flip()
end

That’s the result:

The USB mode makes it easier to edit the map, because you can keep it open in your editor, save it and after a press on “start” you’ll see the result immediatly. Another idea would be a PC based level editor or even better, an in-game editor with Lua Player itself.

Now we need some figure animation:

The program, which shows the player and lets it move, eating points with adding score:

-- load graphics
tiles = Image.load("tiles.png")
figure = Image.load("figure.png")
 
-- the map
mapSource = {
	"mmmmmmmmmmmmmmmmmmmmmmmmmmmmmm",
	"mmmmmmmmmmmmmmmmmmmmmmmmmmmmmm",
	"mmeaaaaaaaaaaaaaaaaaaaaaaaafmm",
	"mmdpnnnnnnnnnnnnnnnnnnnnnnpbmm",
	"mmdokcccccclnkccccccccccclnbmm",
	"mmdojaaaaafdnbmmmmmmmmmmmdnbmm",
	"mmdooooooobdnbmmmmmmmmmmmdnbmm",
	"mmdokccclobdnbmmmmmmmmmmmdnbmm",
	"mmdobmmmdobdnbmmmmmmmmmmmdnbmm",
	"mmdobmmmdobdnjaaaaaaaaaafdnbmm",
	"mmdobmmmdpbdnnnnnnnnnnnnbdnbmm",
	"mmdobmmmhcghcccccccccclnbdnbmm",
	"mmdojaaaaaaaaaaaaaaaaainjinbmm",
	"mmdpnnnnnnnnnnnnnnnnnnnnnnpbmm",
	"mmhccccccccccccccccccccccccgmm",
	"mmmmmmmmmmmmmmmmmmmmmmmmmmmmmm",
	"mmmmmmmmmmmmmmmmmmmmmmmmmmmmmm"
}
 
-- offsreen image, where the map will be drawn
board = Image.createEmpty(480, 272)
 
-- draw one tile on the map
function drawTile(tile, x, y)
	tile = string.byte(tile, 1) - string.byte("a")
	local tileX = math.mod(tile, 4)
	local tileY = math.floor(tile / 4)
	board:blit(16 * x, 16 * y, tiles, 17 * tileX + 1, 17 * tileY + 1, 16, 16, false)
end	
 
-- copy the map source to an array for easier access and draw the offscreen board
map = {}
for y = 1, 17 do
	line = mapSource[y]
	map[y] = {}
	for x = 1, 30 do
		tile = string.sub(line, x, x)
		map[y][x] = tile
		drawTile(tile, x - 1, y - 1)
	end
end
 
-- current player position
playerX = 6
playerY = 14
 
-- current player animation image
animation = 0
 
-- 1 = animation images increases, -1: decreases
animationDirection = 1
 
-- animation slowdown counter
animationSlowDown = 0
 
-- controls the movement speed. At speedStep == 0, the last
-- pad will be evaluated, other steps the pad movements are only
-- saved
speedStep = 0
 
-- last pad direction
dx = 0
dy = 0
 
-- current score
score = 0
 
-- text color
white = Color.new(255, 255, 255)
 
while true do
	-- read current pad
	pad = Controls.read()
	
	if pad:start() then
		-- exit to lowser, if started from there
		break
	end
	if speedStep == 0 then
		-- save old player position
		oldPlayerX = playerX
		oldPlayerY = playerY
		
		-- add last movement
		playerX = playerX + dx
		playerY = playerY + dy
		
		-- check new tile
		newTile = map[playerY][playerX]
		
		-- if the position is not allowed, restore old player position
		if newTile ~= "n" and newTile ~= "p" and newTile ~= "o" then
			playerX = oldPlayerX
			playerY = oldPlayerY
		end
		
		-- if it is a tile which can be eaten, eat it
		if newTile == "p" then score = score + 5 end
		if newTile == "o" then score = score + 1 end
		if newTile == "p" or newTile == "o" then
			-- set background tile
			map[playerY][playerX] = "n"
 
			-- draw background tile
			drawTile("n", playerX - 1, playerY - 1)
		end
		
		-- reset last movement
		dx = 0
		dy = 0
	else
		-- check pad movement
		if pad:up() then
			dy = -1
		elseif pad:down() then
			dy = 1
		elseif pad:left() then
			dx = -1
		elseif pad:right() then
			dx = 1
		end
	end
	
	-- update speed step counter
	speedStep = speedStep + 1
	if speedStep == 5 then
		speedStep = -1
	end
	
	-- blit board
	screen:blit(0, 0, board, 0, 0, board:width(), board:height(), false)
	
	-- blit current player animation image
	screen:blit((playerX - 1) * 16 - 4, (playerY - 1) * 16 - 4, figure, 1 + 25 * animation, 1, 24, 24)
	
	-- calculate next animation image
	animationSlowDown = animationSlowDown + 1
	if animationSlowDown == 3 then
		animationSlowDown = 0
		animation = animation + animationDirection
		if animation == 4 then
			animationDirection = -1
		elseif animation == 0 then
			animationDirection = 1
		end
	end
	
	-- print score
	screen:print(10, 260, "Score: " .. score, white)
	
	-- wait for vertical sync and show new screen
	screen.waitVblankStart()
	screen:flip()
end

Note that the tiles are drawn to an offscreen image, because blitting multiple tiles is slower than blitting one image of the same size of all tiles. With this trick we archive the full 60 fps framerate. And instead of some complicated collision detection, we check for the n, p and o tiles, only. The figure is 24×24, not 16×16, to make it a bit more interesting.

Some possible improvements: Add some opponents, use a transparent background for the tiles and draw it over some predefined background, or use multiple masks with multiple background or maps with coordinate positions and parts with different sizes etc., use your creativity :-)

The game project with the two images and the script in one zip file.

 
psp/lua_player/tutorial.txt · Last modified: 2009/04/30 13:10 by shine
 
Recent changes RSS feed Creative Commons License Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki