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: - Don't try to project a point with the same Z coordinate as the camera. - Do remember to invert Y (the vertical) after projecting. Here I could skip that step thanks to symmetry. - Speaking of coordinates, the camera uses a right-hand system, with the Z axis going into the screen, and Y going up. 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: - converting angles from degrees to radians for `sin` and `cos` to use; - rotating the whole thing 90 degrees counterclockwise, because `sin` and `cos` think the default, "zero" angle points at three o'clock, while we want it at twelve o'clock. The rest is mostly bookkeeping, apart from loop handling. And that's all of two steps: - "[" writes down where the loop starts (right after itself) and how many times to loop; - "]" first checks to see if the loop counter has wound down to zero, then it just moves on. Otherwise it deducts one and goes through the loop again. 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.