Vectrex Thrust Source
Vectrex Thrust Source code
Introduction
Here is the complete source code for my game Vectrex Thrust. This game would never have been possible to write without all the information people have shared on the web, so releasing this source code is my way of giving something back and hoping that someone will find it useful.I'm not good at writing documentation, but I figure releasing the source code without any kind of helping information won't help anyone so I'm just writing down the things I would think need explaining. This document is more of a FAQ than a complete documentation. Just email me, or post a comment if you need more information.
Getting started with Vectrex development
The Vectrex is a really great choice of platform when writing a game for a retro console. It is easy to get started, and the hardware is not complicated compared to other consoles and computers from the early eighties. The Vector graphics gives the games an unique look which does not feel as dated as a low-res bitmapped display (although speaking of what is 'dated' is probably besides the point when discussing retrogaming).A good set of links and documentation is at www.vectrex.nl.
The AS09 assembler by Frank A. Vorstenbosch.
The 6809-port of the GCC compiler.
Building and testing Vectrex Thrust
To assemble the source code to a binary you need a 6809 assembler.I developed Vectrex Thrust on a Windows XP computer using the 'AS09' assembler by Frank A. Vorstenbosch, available here: http://www.kingswood-consulting.co.uk/assemblers/
NOTE: I used version 1.32 of AS09. There are compatibility problems with the later 1.40 version. I've emailed Frank about this. Send a email to me if you need the 1.32 version.
Put AS09.exe in the same file as the Thrust Source code and run the batch file 'm.bat' to assemble the binary:
'm.bat' Assembles the source files into thrust.bin
'm.bat 1' Also generate detailed listing file thrust.lst
After you have a binary (thrust.bin) you can test it by running 'r.bat'. The batch file takes an optional parameter:
'r.bat' Starts the game with the MESS-emulator
'r.bat 2' Starts the game with the VecX-emulator
'r.bat 3' Uploads the game to the VecRAM using veccy-win32.exe
'r.bat 4' Starts the game with the VecXMod-emulator
Be aware that r.bat uses hardcoded paths so you need to change them to your own environment.
Source code organisation
I've tried to keep the code readable and in separate modules. I've even written some comments :) There may be a couple of comments that are in Swedish, just email me if you need it translated or better explained.The files:
Thrust.asm - Main program
Thrust_BonusGame.asm - Code for the hidden bonus game
Thrust_Eeprom.asm - Eeprom support (by Alex Herbert)
Thrust_Fx.asm - Graphical effects
Thrust_Levels.asm - Level maps
Thrust_Music.asm - Music player code and data
Thrust_Ship.asm - Code for controlling the ship and pod
Thrust_Sound.asm - Sound effects
Thrust_Text.asm - Text constants and menu code
Thrust_Title.asm - Title screen
Clip.asm - Line clipping subroutine
Clip.c - Line clipping C source code
Clip.h - Line clipping C header file
Def.asm - Macros, constants, structures and RAM-layout.
Thrust world coordinate system
The game uses an 16-bit X and Y coordinate system with the Y-axis pointing downwards. Negative Y is the area of empty space above planet and is represented by blinking stars.Level data format
I considered several ways of organising the level data. Each one had pros and cons.Vector based
+ This would seem to be the natural choice for a vectrex game :)
+ Compact ROM-memory footprint
- How to do collision?
- How to organize data so that the set of visible lines can be found quickly?
Tile based
+ Set of visible lines can be computed in constant time
+ Collision tests of objects against walls in constant time
+ No need for writing a level editor, levels can be written directly with 'db' statments in the assembler source
- Large levels take lots of ROM-space
- Each tile is drawn separately, producing dots at tile edges
By 'constant time' I mean 'does not increase with complexity of current view or level size'. This was important to me because I wanted to be certain that the code was fast enough for handling large levels (like level 6 in the original game) without slowing down too much.
In the end I settled for a tile based design. The main reason being that I had more experience working with tiles and that the other solution would require more research. When I got a tile based code drawing a small test level, I knew it would work with larger levels also. I felt that a vector based approach would end up more complicated with more states and code paths, which is a bad thing when writing assembler code without a debugger. So quite simply: I was lazy :)
See the file 'Thrust_Levels.asm'.
The music
Vectrex Thrust uses the original music and music player written by the great Rob Hubbard for the Atari ST version of Thrust. I disassembled the 68000 binary using AssemPro for Atari ST, and then translated the assembler code by hand. The player could probably be reused to play other Atari ST music by Rob Hubbard, like from the game Warhawk.An example from the source file Thrust_Music. The original 68000 code is commented out and the 6809 translation follows:
; ANDI.B #$F,D0
; SUB.B D0,$34(A1)
; BPL.S L05947A
anda #$f
ldb $34,x
pshs a
subb ,s+
stb $34,x
bpl L05947A
; CLR.B $34(A1)
lda #0
sta $34,x
...and so on
It is pretty much a line for line conversion, slightly more verbose in 6809 because of the limited instruction set and registers. Also the 32-bit adresses and counters of the original is shortened to 16-bits to save rom-space. That the conversion is at all possible is due to the fact that Atari ST and Vectrex have compatible sound chip hardware.
Stack usage
One macro is used in almost every subroutine, from Def.asm:
;Declare local stack frame.
; s1 nr of 1-byte locals,
; s2 nr of 2-byte locals
; bufsize is size of extra buffer (can be omitted)
;Locals are named Local1B etc for byte, Local1W for word, buffer is named LocalBuffer.
mDecLocals macro s1,s2,bufsize
This is a very useful macro for defining local variables.
Example of use:
ShowTitleScreen:
;This subroutine uses 7 local byte sized variables named LocalB[1..7]
mDecLocals 7,0,0
;Give the locals some useful names
LocalTextTimer = LocalB1
LocalTextInt = LocalB2
LocalMusicTimer = LocalB3
LocalVolumeTimer = LocalB4
LocalMode = LocalB5
LocalModeClock = LocalB6
LocalBonusGame = LocalB7
...
;Code using the local vars
tst LocalBonusGame,s
...
;End of subroutine, remove stack frame
mFreeLocals
rts
Drawing a scrolling landscape
Thrust uses a scrolling game world. It is not constantly scrolling, instead it detects when the ship is close to the screen border and starts scrolling the view until the ship is again at a preset minimum distance to the border.
Here is an explanation of the routines involved in drawing the landscape.
SetView()
This routine does the following:
- If currently scrolling then update ViewX/Y pair
- Else check if ship is close to screen border, if so then start scrolling
The NeedRefresh byte is set if the view coordinated are changed.
RefreshDrawList()
This is probably the largest subroutine in the game. Here is some pseudo code of what it is doing:
Test the NeedRefresh byte, exit if not set
Determine the set of visible tiles using the ViewX/Y pair.
For each visible tile
if tile needs clipping (a border tile)
set up parameters and call clip-subroutine
the clip-subroutine writes clipped lines to DrawList-memory
else
copy lines of tile to DrawList-memory
Write a zero to DrawList to mark end of list
DrawList now contains the set of visible lines for the current screen.
DrawLevel()
Call GoDrawList() with DrawList-memory as parameter
GoDrawList() simply draws every line in the drawlist, these are in screen coordinates so they can be drawn directly without processing.
The DrawList-memory is kept between frames if the view is not scrolling, this saves a lot of cpu time since RefreshDrawList() can simply exit: the visible lines are the same as last frame.
Clipping
The lines of the tiles at the screen border needs to be clipped from world-coordinates into screen coordinates. A general 2d line clipping routine is quite slow as it uses division. Fortunately it can be very specialized to match the requirements for Thrust. We can hardcode it to handle three types of lines:- Vertical
- Horizontal
- Fixed 22.5 degree slope
Clipping these kinds of lines is trivial. Vertical and horizontal just need to check that the max x or y coordinate is lower than the allowed clipping region. A sloped line needs a bit more work: shorten the line in the x-direction with half the clipped amount of the y-direction (and the other way around). A sloped line in a screen corner may need to clip both ends. I started with a general Cohen-Sutherland clipping code I found on the internet and then simplified it and made it work with the GCC for 6809 compiler. The compiler source output (gcc09 -S) was then saved to Clip.asm. Line clipping is also used by the hidden bonus game.
Collision detection
The PointVsLevel subroutine in Thrust.asm handles collision against planet walls.
Parameters:
;x and y are coords to test (world coordinates)
;returns carry set if collision
Exact tests can be made by looking up what tile is at X/Y and then call the collision subroutine for that particular tile, passing along the coordinates relative to the top left corner of the tile. Because of the limitation in line types (horizontal, vertical or fixed slope) determining if collision has occurred within a tile is simple.
This image shows three different tiles and subroutines.
There is a total of fifteen different tile collision subroutines used in the game.Six of these are for handling tiles that contains sliding doors.
Collision tests between sprites are made with simple rectangle intersections tests. See the macro mTestAreaVsArea defined in Def.asm and example of usage in Thrust_Ship.asm.
Demo mode
Thrust uses recorded gameplay for the demomode. I downloaded the source for the VecX emulator and modified it to record the joystick and button state. When the emulator exits it dumps the recorded data to a textfile which can be inserted in a assembler file.Vecxmod.exe is the modified binary. Thrust needs to be assembled with a special flag set when recording input, set DemoRecord=1 in Def.asm. This makes Thrust call an illegal 6809 opcode once every frame during gameplay. Vecxmod catches this special opcode and records the input. This way no "noise" input needs to be removed afterwards, only gameplay input is dumped to the file.
See the file Thrust_Ship.asm for how the demo data is used:
;Recorded demo data
;First two bytes: start level, reserved
DemoData1:
db 0,0
db $00, $0b, $10, $07, $00, $01, $0c, $07, $20, $09, $2c, $0c, $0c, $0a, $2c, $03
db $20, $08, $21, $00, $00, $08, $01, $00, $00, $05, $10, $00, $00, $00, $01, $00
...
The format is: inputbits, inputcount. Inputbits is one byte holding the state of joystick one and the four buttons. Inputcount is the nr of frames this state is valid.
Credits
Source and documentation by Ville Krumlinde.EEPROM (highscore battery backup) code by Alex Herbert.
Music code and data originally by Rob Hubbard
Clever frame interval test routine by Thomas Jentzsch.
Vectrex Thrust logo by Manu Pärssinen.
Beta testers: Thomas Jentzsch, Manu Pärssinen, Mark Shaker, Alex Herbert and Rob Mitchell.
Vectrex Thrust on github