More useful tricks for sdlBasic games

2018-04-13

When I wrote my sdlBasic programming guide (which you should read first, by the way), I had definite plans for a third game made with it. Even after those plans fell through, sdlBasic still remains my favorite game development tool nobody's heard of. And as it turns out, in time I gathered a nice little collection of routines that could be useful in all kinds of places. Warning, lots of code ahead -- in my defense, it's all fully functional, tested demos, and while they can get long, it's all straightforward. I wouldn't have it any other way.

2.5D camera

2D games are all good and well, and sdlBasic has excellent support for them. But sometimes you want to add just a little depth, without going full 3D. While for a parallax background you can just wing it, when you have to track dozens or hundreds of objects moving at different depths, you pretty much need the real deal -- actual perspective projection.

Mind you, my camera is pretty simplistic: it can't tilt or rotate, and you have to fudge the "focal" distance -- actually more like the body depth of a pinhole camera. (Tip: keep it about equal to the height of your window, more or less. Here it should be less.) I still used it successfully in many games, across four different programming languages.

	option explicit

	const scrwidth = 1024
	const scrheight = 768

	setdisplay(scrwidth, scrheight, 32, 1)

	dim common cam_x = 0, cam_y = 0, cam_z = -512
	dim common cam_f = scrheight
	dim common cam_px = 0, cam_py = 0, cam_scale = 0

	sub cam_project(x, y, z)
		dim x1 = x - cam_x
		dim y1 = y - cam_y
		dim z1 = z - cam_z

		cam_scale = cam_f / z1
		cam_px = x1 * cam_scale
		cam_py = y1 * cam_scale
	end sub

	dim x, y, z

	for z = 256 to 32 step -32
		for y = -256 to 256 step 64
			for x = -256 to 256 step 64
				cam_project(x, y, z)
				ink(rgb(256 - z, 256 - z, 256 - z))
				fillcircle(cam_px + 512, cam_py + 384, cam_scale * 24)
			next
		next
	next

	waitkey

For the sdlBasic implementation, my first instinct was to do it the object-oriented way, storing all those variables in an associative array passed by reference to cam_project, but that was very slow, and not worth the trouble when you have just one camera anyway, as you're likely to. So for this article I switched to a procedural style. No need to try and force things.

A few more tips:

Game menus

This one involves a lot more code, but on the plus side it's much easier to explain. In fact, to some programmers it may seem obvious. But remember that one person's obvious is another's inscrutable. So here's how to set up a typical game menu, that you can navigate with up-down-enter.

	const scrwidth = 800
	const scrheight = 600

	const centerx = 400
	const centery = 300

	dim common main_menu[0 to 7]
	dim common active_item = 1

	dim common game_title

	sub prerender_text()
		ink(rgb(255, 255, 255))
		game_title = textrender("Game menu demo", 60)

		main_menu[0] = textrender(">               <", 30)
		main_menu[1] = textrender("Start", 30)
		main_menu[2] = textrender("Resume", 30)
		main_menu[3] = textrender("Scores", 30)
		main_menu[4] = textrender("Options", 30)
		main_menu[5] = textrender("Help", 30)
		main_menu[6] = textrender("Credits", 30)
		main_menu[7] = textrender("Quit", 30)
		
		dim i
		for i = 0 to 7
			hotspot(main_menu[i], 1, 1) ' Anchor to image center
		next
		hotspot(game_title, 1, 1)
	end sub

	sub paint_title_screen()
		dim i
		
		cls()
		pastebob(centerx, scrheight \ 5, game_title)
		for i = 1 to 7
			pastebob(centerx, scrheight \ 3 + i * 30, main_menu[i])
		next
		pastebob(centerx, scrheight \ 3 + active_item * 30, main_menu[0])
	end sub

	sub loop_title_screen()
		dim dirty = true
		
		do
			if dirty then
				paint_title_screen
				dirty = false
			end if
			
			if key(k_escape) then
				exit do
			elseif key(k_down) then
				active_item += 1
				if active_item > 7 then
					active_item = 1
				end if
				dirty = true
				wait(100)
			elseif key(k_up) then
				active_item -= 1
				if active_item < 1 then
					active_item = 7
				end if
				dirty = true
				wait(100)
			elseif key(k_return) or key(k_space) then
				select case active_item
					case 1
						wait(100)
						print("Start selected")
					case 2
						wait(100)
						print("Resume selected")
					case 7
						exit do
					case else
						wait(100)
						print("Some option selected")
				end select
			end if

			waitvbl
		loop
	end sub

	setdisplay(scrwidth, scrheight, 32, 1)
	prerender_text
	loop_title_screen

This is probably overengineered. I could easily get away with rendering the menu again on every frame, whether it's needed or not. Maybe even with rendering all the text again on the fly. I just don't like waste.

Note that the menu demo relies on sdlBasic loading a default font from the operating system. It no longer does that as of the 2016 build, so you may have to bring your own.

Turtle graphics in one function

I saved the longest and most complex code for the end. You see, sdlBasic is optimized for use with sprites; in fact, its drawing primitives are noticeably slow. But many of my games have vector graphics, for various reasons. And vector graphics have a bit of a problem: all but the simplest shapes take a fair bit of code to generate, code that makes your game bigger and buggier.

The solution would be a miniature drawing language embedded in your game, similar to the DRAW statement in QBasic and GWBasic, or the path syntax in SVG. Trouble is, I don't like either. Too big, with many codes to remember and not so easy to parse. So what to do?

Turtle graphics, that's what. Having coded a simple version already, I knew it didn't take much. The trick is implementing a tiny interpreter, for an equally tiny language. All in one function! Explanations after the demo.

	option explicit

	function turtle_draw(x, y, drawing$)
		' Preserve the origin for the x and y commands.
		dim tx = x
		dim ty = y
		dim heading = 0
		
		dim is_pen_down = true
		
		dim data_stack[1 to 10]
		dim ret_stack[1 to 10]
		dim d_sp = 0
		dim r_sp = 0
		dim pc = 1
		
		dim buffer = ""
		dim signum = 1
		dim angle = 0
		dim dx = 0
		dim dy = 0

		while pc <= len(drawing$)
			if mid$(drawing$, pc, 1) = "-" then
				signum = -1
				pc += 1
				if pc > len(drawing$) then
					print "Number expected at position " + str$(pc)
					return false
				end if
			else
				signum = 1
			end if
			
			buffer = ""
			dim tmp_ch = mid$(drawing$, pc, 1)
			while tmp_ch >= "0" and tmp_ch <= "9"
				buffer += tmp_ch
				pc += 1
				if pc > len(drawing$) then
					return false
				else
					tmp_ch = mid$(drawing$, pc, 1)
				end if
			wend
			
			if len(buffer) > 0 then
				buffer = val(buffer) * signum
			else
				buffer = 0
			end if
			
			select case mid$(drawing$, pc, 1)
				case "f"
					angle = (heading - 90) * (3.141592 / 180)
					dx = cos(angle) * buffer
					dy = sin(angle) * buffer
					if is_pen_down then
						line(tx, ty, tx + dx, ty + dy)
					end if
					tx += dx
					ty += dy
				case "b"
					angle = (heading - 90) * (3.141592 / 180)
					dx = cos(angle) * buffer
					dy = sin(angle) * buffer
					if is_pen_down then
						line(tx, ty, tx - dx, ty - dy)
					end if
					tx -= dx
					ty -= dy
				case "l"
					heading = (heading - buffer) mod 360
				case "r"
					heading = (heading + buffer) mod 360
				case "h"
					heading = buffer mod 360
				case "u"
					is_pen_down = false
				case "d"
					is_pen_down = true
				case "x"
					' Move relatively to origin, not turtle.
					if is_pen_down then
						line(tx, ty, x + buffer, ty)
					end if
					tx = x + buffer
				case "y"
					' Move relatively to origin, not turtle.
					if is_pen_down then
						line(tx, ty, x, ty + buffer)
					end if
					tx = x + buffer
				case "["
					if d_sp >= 10 or r_sp >= 10 then
						print "Stack overflow."
						return false
					end if
					d_sp += 1
					data_stack[d_sp] = buffer
					r_sp += 1
					ret_stack[r_sp] = pc
				case "]"
					if d_sp < 1 or d_sp < 1 then
						print "Stack underflow."
						return false
					end if
					if data_stack[d_sp] <= 1 then
						d_sp -= 1
						r_sp -= 1
					else
						data_stack[d_sp] -= 1
						pc = ret_stack[r_sp]
					end if
				case else
					print "Unknown drawing instruction: " +
						mid$(drawing$, pc, 1)
					return false
			end select
			
			pc += 1
		wend
		return true
	end function

	setdisplay(1024, 768, 32, 1)

	turtle_draw(128, 32, "160r5[128f144r]")
	turtle_draw(512, 384, "10[4[256f90r]36r]")
	turtle_draw(1024-128, 768-160, "160r5[128f144r]")

	waitkey

To figure out the code, first look at the bottom. Each drawing consists mainly of commands in the form "NNNc": an integer with optional sign followed by a letter. Square brackets mark loops, like the REPEAT statement in Logo; a number before the opening square bracket says how many times to go around. And because loops can have loops, like the second example, it needs a data stack and a return stack, to hold the loop counter and where in drawing$ each loop begins, respectively. The pc variable tracks where we are in drawing$ at any one point -- which character we'll read next.

Now for how it works: the first part of the while, before the select (lines 24-51), parses a signed integer. You could get rid of the whole mess by expecting just one digit there, if you don't mind tiny, blocky drawings. The real magic happens in the handling of "f" and "b" commands ("forward" and "back"). You might want to brush up on your trigonometry 101, but in essence they start from a point and an angle, draw a line of given length (held in buffer), and wherever the line ends becomes the new starting point. Most of the complications arise from two sources:

The rest is mostly bookkeeping, apart from loop handling. And that's all of two steps:

Okay, this last one was maybe too much for a mere bag of tricks. But tell me it isn't cool! Note how you don't need to worry about color because you can set it with ink before calling turtle_draw. And while you can't set the line thickness, you can draw the same thing repeatedly, moving it by one pixel every time.

In conclusion, you don't need a fancy programming language to pull off some nice special effects. The humble sdlBasic can take you surprisingly far. So have fun with it.

Tags: ,