Part 1: Vectorising the background (see below)
Part 2: Vectorising the character/fonts (
link to post )
Part 3a: Vectorising some of the sprites (
link to post )
Part 3b: Vectorising Kong's sprites (
link to post )
Part 3c: Vectorising Jumpman's sprites (
link to post )
Part 4: Adding a title screen and finishing things off (
link to post )
I'm having a go at vectorising Donkey Kong for MAME. I'll be hacking the original game, suppressing the normal pixel output completely and rendering high resolution vectors to the screen. The techniques i'll be using could be applied to other classic arcade games. I'm choosing Donkey Kong because i'm very familiar with the disassembled code and mechanics of the game.
I'm documenting my journey and hopefully you may find it interesting and follow along.
I took inspiration from this blog which considers if a Donkey Kong port would be feasible on the Vectrex.
http://vide.malban.de/november-21st-donkey-kong-wait-noMy plan is to use the simple vector drawing capabilities of MAME LUA scripting language (available in MAME from version 0.196 to current).
There are some limitations. All vectors must be drawn out every frame and there are 60 frames per second. Optimisation is key and i'll have to do some calculations before I get carried away. I'll work out how many vectors can be safely drawn each frame without compromising the emulation speed.
First we'll need to write some LUA code to instantiate the emulated screen device in MAME so we can draw to it.
scr = manager.machine.screens[":screen"]
Then we'll need a simple function to draw a vector. This function draws a line from x,y position to another x,y position. Anything we draw, including other shapes, will me made up of vectors and will use this function.
function vector(y1, x1, y2, x2)
scr:draw_line(y1+wobble(), x1+wobble(), y2+wobble(), x2+wobble(), intensity())
vector_count = vector_count + 1
end
We'll keep tally of the number of vectors we draw.
Notice the funky "wobble" and "intensity" options. These are used to make vectors appear less boring on screen.
We can make a polyline function to draw multiple vectors that are chained together like etch-a-sketch
function polyline(data)
-- draw multiple chained lines from a table of x, y data points
local _y, _x
for _i=1, #data, 2 do
if _y and _x then vector(data[_i], data[_i+1], _y, _x) end
_y, _x =data[_i], data[_i+1]
end
end
We can make a function to draw a box by chaining 4 lines with our polyline function. We can specify the height and width of the box.
function box(y, x, h, w)
-- draw a simple box at given position with height and width
polyline({y,x,y+h,x,y+h,x+w,y,x+w,y,x})
end
We can make a function to draw a circle by chaining lots of vectors together. I ran with 20 vectors per circle.
function circle(y, x, r)
-- draw a 20 segment circle at given position with radius
local _save_segy, _save_segx
for _segment=0, 360, 18 do
local _angle = _segment * (math.pi / 180)
local _segy, _segx = y + r * math.sin(_angle), x + r * math.cos(_angle)
if _save_segy then vector(_save_segy, _save_segx, _segy, _segx) end
_save_segy, _save_segx = _segy, _segx
end
end
We now have our 4 drawing functions so lets try to max out MAME to see how many vectors we can draw per frame.
I made a function for testing the limits. This function will cycle through 4 different drawing tests (lines, polylines, boxes and circles). We can increase the limit gradually and keep a check on MAME average emulation speed (Function key F11).
function debug_limits(limit)
local _rnd, _ins = math.random, table.insert
local _cycle = math.floor(scr:frame_number() % 720 / 180) -- cycle through the 4 tests, each 3 seconds long
if _cycle == 0 then
for _=1, limit do vector(256, 224, _rnd(248), _rnd(224)) end -- single vectors
elseif _cycle == 1 then
_d={}; for _=0,limit do _ins(_d,_rnd(256)); _ins(_d,_rnd(224)) end; polyline(_d) -- polylines
elseif _cycle == 2 then
for _=1, limit/20 do circle(_rnd(200)+24, _rnd(176)+24, _rnd(16)+8) end -- circles
else
for _=1, limit / 4 do box(_rnd(216), _rnd(200), _rnd(32)+8, _rnd(24)+8) end -- boxes
end
debug_vector_count()
end
Results show we can safely draw 1000 vectors per frame when tested in MAME versions 0.196 and 0.242 running on Windows 10 at 1920x1080. Reducing the resolution increases the limit. 1200 per frame is ok too but let's keep it safely inside the limits. I'll also test on Raspberry Pi 4 later.
This video shows our tests running on top of Donkey Kong attract mode.
I think 1000 vectors should be sufficient. I'll use vectors sparingly and keep tally of the numbers as I progress.
Let's start by building background graphics for the girders stage. Here are functions to draw girders and ladders.
A simple girder is made from 2 parallel lines. We can add detail later if our vector count allows.
function draw_girder(y1, x1, y2, x2)
-- draw parallel vectors (offset by 7 pixels) to form a girder. Co-ordinates relate to the bottom vector.
vector(y1, x1, y2, x2, intensity())
vector(y1+7, x1, y2+7, x2, intensity())
end
A ladder is a more complex object with legs and rungs. We'll need to deal with broken ladders too. We can treat broken ladders as 2 separate ladders that are set slightly apart.
function draw_ladder(y, x, h)
-- draw a single ladder at given y, x position of given height in pixels
vector(y, x, y+h, x) -- left leg
vector(y, x+8, y+h, x+8) -- right leg
for i=0, h-2 do -- draw rung every 4th pixel (skipping 2 pixels at bottom)
if i % 4 == 0 then vector(y+i+2, x, y+i+2, x+8) end
end
end
We now test drawing girders and ladders. In total we have used 139 vectors.
Now for some other background detail for the girders stage. The oilcan, hammers and the stacked barrels.
function draw_oilcan(y, x)
box(y, x, 1, 16) -- outline of oil can
box(y+1, x+1, 14, 14) -- bottom lip
box(y+15, x, 1, 16) -- top lip
box(y+7, x+4, 3, 3) -- "O"
vector(y+7, x+9, y+10, x+9) -- "I"
polyline({y+7,x+13,y+7,x+11,y+10,x+11}) -- "L"
vector(y+5, x+1, y+5, x+15) -- horizontal stripe
vector(y+12, x+1, y+12, x+15) -- horizontal stripe
end
function draw_hammer(y, x)
polyline({y+5,x,y+7,x,y+8,x+1,y+8,x+8,y+7,x+9,y+5,x+9,y+4,x+8,y+4,x+1,y+5,x}) -- hammer
box(y, x+4, 4, 1) -- bottom handle
box(y+8, x+4, 1, 1) -- top handle
end
function draw_barrel(y, x)
-- draw an upright/stacked barrel
polyline({y+3,x,y+12,x,y+15,x+2,y+15,x+7,y+12,x+9,y+3,x+9,y,x+7,y,x+7,y,x+2,y+3,x}) -- barrel outline
vector(y+1, x+1, y+1, x+8) -- horizontal bands
vector(y+14, x+1, y+14, x+8)
vector(y+2, x+3, y+13, x+3) -- vertical bands
vector(y+2, x+6, y+13, x+6)
end
Drawing all this stuff increases the vector count to 232. That's pretty good going.
We still have sprites for Jumpman, Pauline, Fireballs, Donkey Kong, hammer smashing, oilcan flames and barrels to deal with. We'll also need to make a vector font to show the scores, level, bonus timer, bonus points and other messages.
I'll look at adding vector sprites in the next part. The vector sprites will have to be cleverly linked to the game logic so they behave similarly to the pixel sprites.
Here are the functions responsible for varying the intensity and wobble of the vectors.
function intensity()
-- we can vary the brightness of the vectors
return ({0xbbffffff, 0xddffffff, 0xffffffff})[math.random(3)]
end
function wobble()
-- random change of the vector offset
return math.random(-40, 60) / 100 -- random change of the vector offset
end
Here's a video showing progress with the girders stage. I've hacked out the original background graphics for now so they don't interfere with our new vector graphics.
This is all monochrome for now, but maybe later we can think about adding splashes of colour.
My experimental code is on github at
https://github.com/10yard/vectorkongJon