The making of X-NONCreated by Teasy
Introduction What you will find here are ways to create a game, helpful methods and techniques to guide your way, and examples of organization and implementation all leading up to an end-result to your liking. Usually, before you create something you need to know at least a little bit what you're going to create. The more you know about the creation process, the less obstacles you will find on your way. Even if you are not (entirely) familiar with, or if you're just a little smart or a fairly quick learner, you won't have any difficulty following this tutorial
I've organized this tutorial a bit like an adventure. There is an outline of what I want, and what I want to achieve is clear, but the paths I choose are completely unset and vary wildly to make the journey a lot more interesting
![]()
Knowledge reference Here's a list of things that might be useful before or after commencing the attack on this tutorial![]()
T-C's Blitz tutorials overview Language orientation, section guide & overview with links. http://www.bettiesart.com/tc/blitz/
BlitzBasic Full Foundation tutorial Includes everything you'd ever want to know about: - variables - conditions - loops - collections - routines - code constructs http://www.bettiesart.com/tc/blitz/blitz.html
Relative positioning in 10 steps Geared towards increasing your awareness of relative coordinates. Using a unique what-you-see-is-what-it-is tutorial style. http://www.bettiesart.com/tc/blitz/relative.html
Project Z: One A simple and short game tutorial especially written to give a good example of: - 2D-motion (vectors) - images created at run-time - variable game-flow and control - simple random handling - variable collection handling - simple artificial intelligence - score and difficulty handling - simple heads-up display - simple collision detection - keyboard and mouse control This game comes in various flavors, but each of them is simply a single source code file. http://www.bettiesart.com/tc/pz1/
Vectors and Sine waves This is an in-depth tutorial about motion in 2D space, and explains the ways of the sine wave as well. Also included are various demonstration programs such as "Asteroids"-style space physics. http://www.bettiesart.com/tc/blitz/vectors.html
Value conversion tutorial In-depth explanation of value offsets and scales, which can be applied to countless scenarios. This also uses the what-you-see-is-what-it-is tutorial style. http://www.bettiesart.com/tc/blitz/conv.html
Project Z: Two An advanced and in-depth game tutorial with many cool features, such as: - smooth space physics - scrolling multi-layered stars - simple game menus - dynamic heads-up display - variable levels and difficulties - special rotated rendered images - simple sound and channel handling - color fading using patterns - RGB color handling - customized text routines - frame limiting - intermediate collision detection - separated code construction - advanced game-flow - multi-level control states - sequence handling - intermediate A.I. - embedded media - variable game speed - advanced timing techniques - virtual screen-edge wrapping - screen-edge wrap collisions - intermediate random handling - and various other tricks Like Project Z: One, this is a single source code file with everything directly included. http://www.bettiesart.com/tc/pz2/
Tilemaps tutorial Includes all the basics, support for accurate object motion, and a fast, optimized pixel-perfect collision detection algorithm. This is yet another single source code file with all in-depth explanations embedded, so some revealing parts are sneakily hidden inside
http://www.bettiesart.com/tc/blitz/tile.html You can also use all of the above simply as reference, so you can look it up if there's something further on in the tutorial that gives you the creeps
There's a bunch of files included in the tutorial package, such as images, audio and source code files to help you try out every step of the way. You can also copy and paste the code ofcourse, if you want. You can download the tutorial files separately (while reading online) for which each linky is provided throughout this tutorial. Or you can download all files together with the tutorial itself as a single package (for offline reading): The making of X-NON.zip. If you want to take a peek at all the files, go to the Tutorial files index. Or if you want go straight to the game that is the final product of this tutorial, take a look at the X-NON files index. And to look for a specific chapter in this tutorial, please refer to the Index.
Concept What I'm creating is a remake of a retro Amiga game called Gravity Force which is a 2D split-screen shooter for 2 players. Actually I was creating this game just to be able to show it to people to find out what the original Amiga game was called.I started by writing down everything I knew about the game, as detailed as I could. Visualizing your ideas for example by writing them down or making sketches can be a great help! To keep at it, I kept trying to remember the feeling that lead me to making this game. So basically, make it interesting for yourself! But keep it simple at first, you can always add more. I found it harder to simplify something that is complicated than making something simple more advanced.
Construct Games usually have some very distinctive parts like various types of media (graphics, sound, music), code and other game data (tilemaps, text). You can find a lot of a game's elements just by looking at the game's description: X-NON is a 2-player split screen shooter with 2D "Asteroids" physics and gravity.
Specification While I was thinking about the game, here's what I wrote down for X-NON on paper: Split screen:
,---------------.---------------. | | | | | | | | | | | | | Player 1 | Player 2 | | | | | | | | | | | | | `---------------^---------------' |
Spawn (loud) Thrust (soft) Fire (loud) Bullet collide (soft) Explode (loud) |
,-----,
| ^ |\
| | | Fire
| | |
,-----+-----+-----,
| < | v | > |\
| | | | | Rotate Left - Thrust - Rotate Right
| | | | |
t-----t-----t-----< |
\ \ \ \|
`-----^-----^-----'
|
/\ Ship o Bullet [] Tile X Destroy |
/\ + /\ = X o + /\ = X [] + /\ = /\ X [] + o = o X |
You can variate the creation process.
Like creating some simple ships, then implementing them.
Making some wacky sounds inbetween.
Make it interesting for yourself.
I can't stress this enough,
since I'm used to taking this way too seriously.
Actually you can do a lot simultaneously,
but doing too much at once isn't always good.
If you get some ideas on the way,
instead of rushing right into creating them,
write them down for later.
And you'll have something cool to look forward to as well
Like a list with "things to do when I'm bored"
Or you can just let any new ideas go
if you know how to "re-sync" with that same vibe later
for more new ideas to appear in your mind automagically,
so you can keep a steady focus on the task at hand.
So let's start by implementing one element at a time.
Before implementing something in code you can also test it
in a separate program, to see how and if it actually works,
safely, without disturbing the existing code.
This can also be useful for other things,
such as sound, graphics, tilemaps, etc.
I will focus mostly on the code itself.
The detailed art, sound and music creation process I'll leave up to you,
or perhaps in another tutorial
Besides, there are also other people on this planet
who can help out in those areas
To give an example
of how I can structure the code in this way,
here are a few sections:
(Definition)
- Player
- Constants: ship speed
- Objects: player object
- Globals: player references
- Functions: creating a player
- Tilemap
- Constants: tile size
- Globals: tile map (array)
- Functions: load a tile map
(Initialization)
- Main initialization
(Execution)
- Main loop
This structure is actually a simplified version
of the structure I used in Project Z: Two.
I will further clarify
and expand on all of these
in the upcoming chapters.
Gravity Force was actually vertically split,
having an upper and lower half of the screen for each player.
This seemed unlogical to me because in most screen resolutions
the playing area is more square when horizontally split, right?
Let's take a look at the resolutions one can choose from.
Heck, you can even find out using a Blitz program.
Copy the following program, run it (in debug mode),
and scroll the debug log in Blitz' Debugger to see which resolutions
are available on your videocard.
Modes = CountGfxModes ()
For Mode = 1 To Modes
DebugLog GfxModeWidth ( Mode ) + "x" + GfxModeHeight ( Mode ) + "x" + GfxModeDepth ( Mode )
Next
WaitKey
End
|
Let's do this in a small testing program.
; Ship image handle
Local Image
Graphics 640 , 480
Local Image = LoadImage ( "Ship Pre Final.PNG" )
; Now drawing to the invisible screen
SetBuffer BackBuffer ()
Repeat
; Flip the invisible with the visible screen
Flip
; Clear the invisible screen
Cls
Until KeyHit ( 1 ) ; Escape
; Manual cleanup (not required though)
FreeImage Image
End
|
A nice black screen with 1 key to press (Esc)
Time for some more functionality.
Let's place the ship on a specific location using two variables,
for the X and Y coordinates.
Our gravity is straight down, so from the top of the screen, to the bottom.
The Y coordinate at the top of the screen is 0,
and increases when you go down.
So that's exactly what we want!
Let's start by putting our ship at the bottom of the screen, facing up.
What should happen if the ship would thrust for a short moment (upwards)?
The ship should 'jump' shortly and land back
on the starting position, right?
Is the gravity always in effect?
Yes!
And the gravity is downwards.
So we can just add something to the current Y coordinate.
Y = Y + gravity |
Y = Y - thrust |
Const Gravity = 1 Const Thrust = 3 |
If KeyDown ( 57 ) ; Space
; Thrusting!
End If
|
If Y > 480 Then Y = 480 |
But let's merge what we have for now into a single program
and see what it does, shall we?
For the assembled program,
take a look at the file Ship Gravity 1.BB
in the Tutorial files index.
; Strengths
Const Gravity = 1
Const Thrust = 3
; Ship position
Local X
Local Y
; Ship image handle
Local Image
; Initial X coordinate (middle of screen width)
X = 320
; Initial Y coordinate (bottom of screen)
Y = 480
Graphics 640 , 480
Image = LoadImage ( "Ship Pre Final.PNG" )
MidHandle Image
; Now drawing to the invisible screen
SetBuffer BackBuffer ()
Repeat
; Space is being held down
If KeyDown ( 57 ) ; Space
; Lift off!
Y = Y - Thrust
End If
; Gravity is always in effect
Y = Y + Gravity
; Prevent ship from falling through the bottom
If Y > 480 Then Y = 480
; Display the ship
DrawImage Image , X , Y
; Flip the invisible with the visible screen
Flip
; Clear the invisible screen
Cls
Until KeyHit ( 1 ) ; Escape
; Manual cleanup (not required)
FreeImage Image
End
|
An easy solution is using a vertical speed variable
we can then use for gravity and thrust.
; Thrust using space, upwards If KeyDown ( 57 ) Then speedY = speedY - Thrust ; Apply gravity speedY = speedY + Gravity |
Y = Y + speedY |
If Y > 480
Y = 480
; Stop the ship
speedY = 0
End If
|
; Strengths
Const Gravity = 1
Const Thrust = 3
; Ship position
Local X
Local Y
; Ship speed
Local speedY
; Ship image handle
Local Image
; Initial X coordinate (middle of screen width)
X = 320
; Initial Y coordinate (bottom of screen)
Y = 480
Graphics 640 , 480
Image = LoadImage ( "Ship Pre Final.PNG" )
MidHandle Image
; Now drawing to the invisible screen
SetBuffer BackBuffer ()
Repeat
; Thrust using space, upwards
If KeyDown ( 57 ) Then speedY = speedY - Thrust
; Apply gravity
speedY = speedY + Gravity
; Apply speed to position
Y = Y + speedY
; Prevent ship from falling through the bottom
If Y > 480
Y = 480
; Stop the ship
speedY = 0
End If
; Display the ship
DrawImage Image , X , Y
; Flip the invisible with the visible screen
Flip
; Clear the invisible screen
Cls
Until KeyHit ( 1 ) ; Escape
; Manual cleanup is not mandatory
FreeImage Image
End
|
The strengths are a bit high, but the effect is what we want, right?
We can implement this bit of code in the game later on,
and tweak the strength to be just right.
Now, for something completely different..
Okay, let's make a small program that does this.
I'm using a sound called Test Sound.MP3 (from the package)
to see how to pull this off easily.
; Load the sample and set it to loop Local Sound = LoadSound ( "Test Sound.MP3" ) LoopSound Sound ; Play the sample and keep track of the channel Local Channel = PlaySound ( Sound ) ; Wait for key, then exit WaitKey End |
; Panning ChannelPan Channel , -1 ; Left ChannelPan Channel , 0 ; Center ChannelPan Channel , 1 ; Right |
; Player identification Local PlayerNumber |
; Left player
If PlayerNumber = 1
; Left panning
ChannelPan Channel , -1
; Right player
ElseIf PlayerNumber = 2
; Right panning
ChannelPan Channel , 1
EndIf
|
source offset = 1 source scale = 1 |
target offset = -1 target scale = 2 |
target value = source value - 1 |
target = ( source - 1 ) * 2 |
target = ( source - 1 ) * 2 + -1 |
ChannelPan Channel , ( PlayerNumber - 1 ) * 2 - 1 |
If this bit is confusing, don't worry.
Using a simple 'If Then' is easier to read anyway.
Imagine flying upwards,
then pressing the right arrow key to rotate right,
but still flying upwards.
Now pressing the up arrow key
will slowly shift the direction to the right.
So the velocity vector (current X and Y ship speeds)
is separate from the thrust vector.
And the thrust vector is only significant when thrusting.
Then, when calculating the new ship velocity vector,
you simply add the thrust vector to the current ship velocity vector.
First, setting up a screen resolution.
A bunch of constants for convenience
; Blitz graphics window mode states Const WindowMode_AutoDetect = 0 Const WindowMode_FullScreen = 1 Const WindowMode_Windowed = 2 Const WindowMode_Scaled = 3 ; Desired screen settings Const ScreenWidth = 640 Const ScreenHeight = 480 Const ColorDepth = 0 Const WindowMode = WindowMode_AutoDetect .. ; Setup screen Graphics ScreenWidth , ScreenHeight , ColorDepth , WindowMode SetBuffer BackBuffer () |
; Original ship image (pointing upwards) Local ShipImage ; Temporarily rotated ship (any direction) Local RotatedShipImage .. ; Load and center ship image ShipImage = LoadImage ( "Ship Pre Final.PNG" ) MidHandle ShipImage ; Disable rotation/scaling smoothing TFormFilter False |
; Ship position Local PosX# Local PosY# .. ; Start in center of screen PosX = ScreenWidth / 2 PosY = ScreenHeight / 2 |
; Ship motion control Const KeyUp = 200 Const KeyDown = 208 Const KeyLeft = 203 Const KeyRight = 205 ; Game control Const KeyEsc = 1 |
; Current ship angle Local Angle# |
; - Ship image ; - Duplicate the original image RotatedShipImage = CopyImage ( ShipImage ) ; - Rotate it to the current angle RotateImage RotatedShipImage , Angle ; - Then draw it on screen DrawImage RotatedShipImage , PosX , PosY ; - And clear the rotated image FreeImage RotatedShipImage |
; Ship rotation speed in degrees per frame
Const RotateSpeed = 4
..
; - Rotate left
If KeyDown ( KeyLeft )
Angle = Angle - RotateSpeed
If Angle < 0 Then Angle = Angle + 360
EndIf
; - Rotate right
If KeyDown ( KeyRight )
Angle = Angle + RotateSpeed
If Angle >= 360 Then Angle = Angle - 360
EndIf
|
Dim Images( 360 ) |
; Trigonometry: return horizontal vector
Function VectorX# ( Distance# , Angle# )
Return Sin ( Angle ) * Distance
End Function
; Trigonometry: return vertical vector
Function VectorY# ( Distance# , Angle# )
Return Sin ( Angle - 90 ) * Distance
End Function
; Trigonometry: return distance using vector
Function VectorDistance# ( X# , Y# )
Return Sqr ( X * X + Y * Y )
End Function
; Trigonometry: return angle using vector
Function VectorAngle# ( X# , Y# )
Return 180 - Atan2 ( X , Y )
End Function
|
; Current ship heading (direction) Local Heading# ; Current ship speed (velocity) Local Speed# |
; Ship speed vector Local SpeedX# Local SpeedY# |
; Ship thrust speed in pixels per frame Const ThrustSpeed# = 0.12 .. ; Ship thrust vector Local ThrustX# Local ThrustY# |
; - Thrust
If KeyDown ( KeyDown )
; Combine thrust speed and ship angle to make the thrust vector
ThrustX = VectorX ( ThrustSpeed , Angle )
ThrustY = VectorY ( ThrustSpeed , Angle )
Else
; Reset thrust vector
ThrustX = 0
ThrustY = 0
EndIf
|
; - Take the current velocity vector (speed and heading) ; - Then add the thrust vector to it (thrust and angle) ; - Which becomes the new velocity vector SpeedX = VectorX ( Speed , Heading ) + ThrustX SpeedY = VectorY ( Speed , Heading ) + ThrustY |
; - Now update the current speed and heading with the new velocity vector Speed = VectorDistance ( SpeedX , SpeedY ) Heading = VectorAngle ( SpeedX , SpeedY ) |
; Ship maximum velocity in pixels per frame
Const MaxSpeed = 5
..
; - Limit the speed to the maximum
If Speed > MaxSpeed
Speed = MaxSpeed
; Apply the new speed to the velocity vector
SpeedX = VectorX ( Speed , Heading )
SpeedY = VectorY ( Speed , Heading )
EndIf
|
; - Finally add the velocity vector to the ship position PosX = PosX + SpeedX PosY = PosY + SpeedY |
; Trigonometry: return horizontal vector
Function VectorX# ( Distance# , Angle# )
Return Sin ( Angle ) * Distance
End Function
; Trigonometry: return vertical vector
Function VectorY# ( Distance# , Angle# )
Return Sin ( Angle - 90 ) * Distance
End Function
; Trigonometry: return distance using vector
Function VectorDistance# ( X# , Y# )
Return Sqr ( X * X + Y * Y )
End Function
; Trigonometry: return angle using vector
Function VectorAngle# ( X# , Y# )
Return 180 - ATan2 ( X , Y )
End Function
;-------------------------------------------------------------------------------
; Blitz graphics window mode states
Const WindowMode_AutoDetect = 0
Const WindowMode_FullScreen = 1
Const WindowMode_Windowed = 2
Const WindowMode_Scaled = 3
; Desired screen settings
Const ScreenWidth = 640
Const ScreenHeight = 480
Const ColorDepth = 0
Const WindowMode = WindowMode_AutoDetect
; Ship motion control
Const KeyUp = 200
Const KeyDown = 208
Const KeyLeft = 203
Const KeyRight = 205
; Game control
Const KeyEsc = 1
; Ship thrust speed in pixels per frame
Const ThrustSpeed# = 0.12
; Ship rotation speed in degrees per frame
Const RotateSpeed# = 4
; Ship maximum velocity in pixels per frame
Const MaxSpeed = 5
;-------------------------------------------------------------------------------
; Ship position
Local PosX#
Local PosY#
; Current ship angle
Local Angle#
; Current ship heading (direction)
Local Heading#
; Current ship speed (velocity)
Local Speed#
; Ship speed vector
Local SpeedX#
Local SpeedY#
; Ship thrust vector
Local ThrustX#
Local ThrustY#
; Original ship image (pointing upwards)
Local ShipImage
; Temporarily rotated ship (any direction)
Local RotatedShipImage
;-------------------------------------------------------------------------------
; Setup screen
Graphics ScreenWidth , ScreenHeight , ColorDepth , WindowMode
SetBuffer BackBuffer ()
; Load and center ship image
ShipImage = LoadImage ( "Ship Pre Final.PNG" )
MidHandle ShipImage
; Disable rotation/scaling smoothing
TFormFilter False
; Start in center of screen
PosX = ScreenWidth / 2
PosY = ScreenHeight / 2
Repeat
; Input
; - Rotate left
If KeyDown ( KeyLeft )
Angle = Angle - RotateSpeed
If Angle < 0 Then Angle = Angle + 360
EndIf
; - Rotate right
If KeyDown ( KeyRight )
Angle = Angle + RotateSpeed
If Angle >= 360 Then Angle = Angle - 360
EndIf
; - Thrust
If KeyDown ( KeyDown )
; Combine thrust speed and ship angle to make the thrust vector
ThrustX = VectorX ( ThrustSpeed , Angle )
ThrustY = VectorY ( ThrustSpeed , Angle )
Else
; Reset thrust vector
ThrustX = 0
ThrustY = 0
EndIf
; Update
; - Take the current velocity vector (speed and heading)
; - Then add the thrust vector to it (thrust and angle)
; - Which becomes the new velocity vector
SpeedX = VectorX ( Speed , Heading ) + ThrustX
SpeedY = VectorY ( Speed , Heading ) + ThrustY
; - Now update the current speed and heading with the new velocity vector
Speed = VectorDistance ( SpeedX , SpeedY )
Heading = VectorAngle ( SpeedX , SpeedY )
; - Limit the speed to the maximum
If Speed > MaxSpeed
Speed = MaxSpeed
; Apply the new speed to the velocity vector
SpeedX = VectorX ( Speed , Heading )
SpeedY = VectorY ( Speed , Heading )
EndIf
; - Finally add the velocity vector to the ship position
PosX = PosX + SpeedX
PosY = PosY + SpeedY
; - Screen edge wrap when crossing screen boundaries
If PosX < 0
PosX = PosX + ScreenWidth
ElseIf PosX >= ScreenWidth
PosX = PosX - ScreenWidth
EndIf
If PosY < 0
PosY = PosY + ScreenHeight
ElseIf PosY >= ScreenHeight
PosY = PosY - ScreenHeight
EndIf
; Render
Cls
; - Ship image
; - Duplicate the original image
RotatedShipImage = CopyImage ( ShipImage )
; - Rotate it to the current angle
RotateImage RotatedShipImage , Angle
; - Then draw it on screen
DrawImage RotatedShipImage , PosX , PosY
; - And clear the rotated image
FreeImage RotatedShipImage
Flip
Until KeyHit( KeyEsc )
; Blitz automagically cleans up
End
|
; Attempt to load the sound Local Sound = LoadSound ( "Thrust.WAV" ) If Not Sound Then RuntimeError "Unable to load sound" ; Make it loop, start playing it, and pause it LoopSound Sound Local Channel = PlaySound ( Sound ) PauseChannel Channel |
; Test channel pause/resume
Repeat
If KeyDown ( 57 ) ; Space
ResumeChannel Channel
Else
PauseChannel Channel
End If
Until KeyHit ( 1 ) ; Esc
|
; Size of a single tile Const TileWidth = 32 Const TileHeight = 32 ; Number of tiles in anim image Const TileCount = 42 |
; Tiles images Global TileStrip ; ( 1-based tile position index ) Dim Tilemap( 0 , 0 ) Global TilemapWidth Global TilemapHeight .. ; Load tiles images TileStrip = LoadAnimImage ( "Tiles.PNG" , TileWidth , TileHeight , 0 , TileCount ) |
Function LoadTilemap ( FileName$ )
Local FileHandle
Local PosX
Local PosY
Local Line$
Local TileNumber
; Attempt to open file (return False if unsuccesful)
FileHandle = OpenFile ( FileName )
If Not FileHandle Then Return False
; Grab tilemap dimensions
TilemapWidth = ReadLine ( FileHandle )
TilemapHeight = ReadLine ( FileHandle )
; Reset tilemap
Dim Tilemap( TilemapWidth , TilemapHeight )
For PosY = 1 To TilemapHeight
Line = ReadLine ( FileHandle )
For PosX = 1 To TilemapWidth
; Convert tilenumber in map to framenumber in anim image (chr 33 = tile 0)
TileNumber = Asc ( Mid ( Line , PosX , 1 ) ) - 33
; Discard tiles which are out of range
If TileNumber >= 0 And TileNumber <= TileCount-1
Tilemap( PosX , PosY ) = TileNumber
End If
Next
Next
CloseFile FileHandle
Return True
End Function
|
20 7 !!!!!!!!!!!!!!!!!!!! ! ! ! ! # % ' ) + - / ! ! ! ! " $ & ( * , . 0 ! ! ! !!!!!!!!!!!!!!!!!!!! |
LoadTilemap "Test Tilemap 1.TXT" |
TileStrip: Frame 0 = spawn spot , Frame 1 = Wall , Frame 2 = Vegetation Lower , etc TileMap: Tile 0 = dont display, Tile 1..42 = display frame from TileStrip TileFile: [ ](#32) = Spacing, [!](#33) = Spawnspot, ["](#34) = Frame 1, etc TileFile -> TileMap conversion: #32 (spacing) -> -1 -> out of range -> 0 (dont display) #33 (spawn spot) -> 0 -> create spawn spot -> 0 (dont display) #34 (frame 1) -> 1 -> on tilemap -> 1 (display) #35 (etc) |
; Discard tiles which are out of range
If TileNumber >= 0 And TileNumber <= TileCount-1
; This is a spawn spot (tile 0)
; Add it to the list and dont put in tilemap
If TileNumber = 0
; Tilemap scale
AddSpawnSpot PosX , PosY
SpawnSpots = SpawnSpots + 1
; Add all other tiles to tilemap
Else
Tilemap( PosX , PosY ) = TileNumber
End If
End If
|
Type SpawnSpot
; Position
Field PosX
Field PosY
End Type
..
; Total number of spawn spots in tilemap
Global SpawnSpots
..
Function AddSpawnSpot ( PosX , PosY )
Local This.SpawnSpot
This = New SpawnSpot
This\PosX = PosX
This\PosY = PosY
End Function
|
Function LoadTilemap ( FileName$ )
..
; Reset tilemap
Dim Tilemap( TilemapWidth , TilemapHeight )
; And spawn spots
Delete Each SpawnSpot
SpawnSpots = 0
|
Function ResAvail ( Width , Height )
For ColorDepth = 16 To 32 Step 8
If GfxModeExists ( Width , Height , ColorDepth ) Then Return ColorDepth
Next
End Function
|
) should support all video cards.
Let's add a bit of a test bed for this function.
; Blitz graphics window mode states
Const WindowMode_Autodetect = 0
Const WindowMode_Fullscreen = 1
Const WindowMode_Windowed = 2
Const WindowMode_Scaled = 3
..
; Graphics resolution
Global ScreenWidth
Global ScreenHeight
Global WindowMode
Global ColorDepth
..
; Windowed in debug mode, otherwise fullscreen
WindowMode = WindowMode_AutoDetect
; Prefered
ScreenWidth = 1280
ScreenHeight = 720
ColorDepth = ResAvail ( ScreenWidth , ScreenHeight )
If Not ColorDepth
; Alternative
ScreenWidth = 800
ScreenHeight = 600
ColorDepth = ResAvail ( ScreenWidth , ScreenHeight )
If Not ColorDepth
; Bare minimum: Small view!
ScreenWidth = 640
ScreenHeight = 480
ColorDepth = ResAvail ( ScreenWidth , ScreenHeight )
End If
End If
Graphics ScreenWidth , ScreenHeight , ColorDepth , WindowMode
WaitKey
End
|
The top (definition) of this code could be placed
in a section for Graphics handling.
While the bottom of this code (execution)
could be placed somewhere at the beginning
of the Main initialization section,
because all other graphics depend on it,
such as LoadAnimImage and LoadImage.
; Sound channel tracker
Type ActiveChannel
; Channel containing the sound playing
Field ChannelHandle
; Reference to the sound being played
; (each sound has a different address)
Field SoundHandle
; Time when started playing
Field Created
; How long to wait before being able to play same sound.
Field TimeOut
End Type
|
Function PlaySample ( SoundHandle , TimeOut = False )
Local FindChannel.ActiveChannel
Local FoundChannel.ActiveChannel
Local Playing = False
Local PlayNew = False
; Check all previously playing sounds/channels
For FindChannel = Each ActiveChannel
; If this channel is still playing
If ChannelPlaying ( FindChannel\ChannelHandle )
; And the sound being played is identical to the new sound to play.
If FindChannel\SoundHandle = SoundHandle
; Remember that it is already being played
Playing = True
; And store it for later access
FoundChannel = FindChannel
End If
; This channel is no longer playing
Else
; Remove it from the list
Delete FindChannel
End If
Next
; The new sound to play is already playing (it was found in the list)
If Playing
; If the time since creation exceeds the channel's overlap (time out)
If MilliSecs - FoundChannel\Created >= FoundChannel\TimeOut
; Playing the new sound is allowed (overlapping)
PlayNew = True
End If
; The new sound to play is not yet playing
Else
; So playing the new sound is allowed (without hesitation)
PlayNew = True
End If
; Allowed to play new sound
If PlayNew
; Create an object to track the channel
FoundChannel = New ActiveChannel
; Remember the time of creation
FoundChannel\Created = MilliSecs
; Record the overlapping time
FoundChannel\TimeOut = TimeOut
; Also remember which sound is used
FoundChannel\SoundHandle = SoundHandle
; Play the sound (and keep track)
FoundChannel\ChannelHandle = PlaySound ( SoundHandle )
; Finally return the channel,
; so you can optionally modify its properties.
Return FoundChannel\ChannelHandle
End If
End Function
|
It is included in a test program Sound Overlap.BB,
located in the Tutorial files index,
which you can check out to see how to operate the routine.
Local sound = LoadSound ( "Spawn.WAV" )
If Not sound Then RuntimeError "Unable to load sound"
Repeat
; press space to play sound with overlap protection
If KeyDown ( 57 )
PlaySample sound , 500
End If
Until KeyHit ( 1 )
End
|
Global MilliSecs |
MilliSecs = MilliSecs () |
Type Bullet
; Time of creation
Field TimeCreated
; Duration of life
Field TimeToLive
; Position
Field PosX#
Field PosY#
; Velocity
Field VelX#
Field VelY#
End Type
|
; Bullet time to live in milliseconds Const BulletTime = 5000 |
Function CreateBullet ( PosX , PosY , VelX# , VelY# )
Local This.Bullet
This = New Bullet
This\PosX = PosX
This\PosY = PosY
This\VelX = VelX
This\VelY = VelY
This\TimeCreated = MilliSecs
; Use default time to live
This\TimeToLive = BulletTime
End Function
|
; Bullet velocity in pixels Const BulletSpeed = 4 |
Function UpdateBullets ()
Local This.Bullet
For This = Each Bullet
; Update position
This\PosX = This\PosX + This\VelX
This\PosY = This\PosY + This\VelY
; Time to die
If MilliSecs - This\TimeCreated >= This\TimeToLive
Delete This
End If
Next
End Function
|
; Simple bullet image Global BulletImage .. ; Load bullet image BulletImage = LoadImage ( "Bullet.BMP" ) MidHandle BulletImage |
; Bullet spawning distance from player image hotspot in pixels Const BulletDistance = 24 |
Function DrawBullets ()
Local This.Bullet
For This = Each Bullet
DrawImage BulletImage , This\PosX , This\PosY
Next
End Function
|
CreateBullet PosX , PosY-BulletDistance , 0 , -BulletSpeed |
; Ship is pointing upwards Const ShipAngle = 0 .. ; Ship position Local ShipPosX Local ShipPosY ; Bullet starting position & speed vector Local BulletPosX Local BulletPosY Local BulletVelX# Local BulletVelY# .. ; Calculate where to create bullet in front of ship BulletPosX = ShipPosX + VectorX ( BulletDistance , ShipAngle ) BulletPosY = ShipPosY + VectorY ( BulletDistance , ShipAngle ) ; Initial bullet velocity vector BulletVelX = VectorX ( BulletSpeed , ShipAngle ) BulletVelY = VectorY ( BulletSpeed , ShipAngle ) CreateBullet BulletPosX , BulletPosY , BulletVelX , BulletVelY |
; Trigonometry: return horizontal vector
Function VectorX# ( Distance# , Angle# )
Return Sin ( Angle ) * Distance
End Function
; Trigonometry: return vertical vector
Function VectorY# ( Distance# , Angle# )
Return Sin ( Angle - 90 ) * Distance
End Function
; Trigonometry: return distance using vector
Function VectorDistance# ( X# , Y# )
Return Sqr ( X * X + Y * Y )
End Function
; Trigonometry: return angle using vector
Function VectorAngle# ( X# , Y# )
Return 180 - ATan2 ( X , Y )
End Function
;-------------------------------------------------------------------------------
; Blitz graphics window mode states
Const WindowMode_AutoDetect = 0
Const WindowMode_FullScreen = 1
Const WindowMode_Windowed = 2
Const WindowMode_Scaled = 3
; Desired screen settings
Const ScreenWidth = 640
Const ScreenHeight = 480
Const ColorDepth = 0
Const WindowMode = WindowMode_AutoDetect
; Bullet control
Const KeySpace = 57
; Game control
Const KeyEsc = 1
;-------------------------------------------------------------------------------
; Bullet spawning distance from player image hotspot in pixels
Const BulletDistance = 24
; Bullet time to live in milliseconds
Const BulletTime = 5000
; Bullet velocity in pixels
Const BulletSpeed = 4
; Ship is pointing upwards
Const ShipAngle = 0
;-------------------------------------------------------------------------------
Type Bullet
; Time of creation
Field TimeCreated
; Duration of life
Field TimeToLive
; Position
Field PosX#
Field PosY#
; Velocity
Field VelX#
Field VelY#
End Type
;-------------------------------------------------------------------------------
; Smart timing wrapper
Global MilliSecs
; Simple bullet image
Global BulletImage
;-------------------------------------------------------------------------------
Function CreateBullet ( PosX , PosY , VelX# , VelY# )
Local This.Bullet
This = New Bullet
This\PosX = PosX
This\PosY = PosY
This\VelX = VelX
This\VelY = VelY
This\TimeCreated = MilliSecs
; Use default time to live
This\TimeToLive = BulletTime
End Function
Function UpdateBullets ()
Local This.Bullet
For This = Each Bullet
; Update position
This\PosX = This\PosX + This\VelX
This\PosY = This\PosY + This\VelY
; Time to die
If MilliSecs - This\TimeCreated >= This\TimeToLive
Delete This
End If
Next
End Function
Function DrawBullets ()
Local This.Bullet
For This = Each Bullet
DrawImage BulletImage , This\PosX , This\PosY
Next
End Function
;-----------------------------------------------------------------------
; Original ship image (pointing upwards)
Local ShipImage
; Bullet starting position & speed vector
Local BulletPosX
Local BulletPosY
Local BulletVelX#
Local BulletVelY#
; Ship position
Local ShipPosX
Local ShipPosY
;-------------------------------------------------------------------------------
; Setup screen
Graphics ScreenWidth , ScreenHeight , ColorDepth , WindowMode
SetBuffer BackBuffer ()
; Load bullet image
BulletImage = LoadImage ( "Bullet.BMP" )
MidHandle BulletImage
; Load and center ship image
ShipImage = LoadImage ( "Ship Pre Final.PNG" )
MidHandle ShipImage
; Place ship in the lower half in the center of the screen
ShipPosX = ScreenWidth / 2
ShipPosY = ScreenHeight * 3 / 4
Repeat
; Timing
MilliSecs = MilliSecs ()
; Input
If KeyDown ( KeySpace )
; Calculate where to create bullet in front of ship
BulletPosX = ShipPosX + VectorX ( BulletDistance , ShipAngle )
BulletPosY = ShipPosY + VectorY ( BulletDistance , ShipAngle )
; Initial bullet velocity vector
BulletVelX = VectorX ( BulletSpeed , ShipAngle )
BulletVelY = VectorY ( BulletSpeed , ShipAngle )
CreateBullet BulletPosX , BulletPosY , BulletVelX , BulletVelY
EndIf
; Update
UpdateBullets
; Render
Cls
DrawImage ShipImage , ShipPosX , ShipPosY
DrawBullets
Flip
Until KeyHit( KeyEsc )
; Blitz automagically cleans up
End
|
; Play fire sound Channel = PlaySample ( FireSound , 10 ) ChannelPan Channel , ( PlayerNumber - 1 ) * 2 - 1 |
;-------------------------------------------------------------------------------
;
; Image Rotator V2
;
;,,,,
; Definition
;----
Const SourceImageFile$ = "Ship Pre Final.PNG"
Const SourceImageWidth = 64
Const SourceImageHeight = 64
Const SourceFrames = 1
Const SourceFrameOffset = 0
Const SourceFrameToUse = 0 ; First frame
Const SourceMask = False
Const SourceMaskRed = 255
Const SourceMaskGreen = 0
Const SourceMaskBlue = 255
Const RemoveMaskColor = True
Const DestinationFile$ = "Ship Images\Ship "
Const RotationInterval = 5
Const AntiAliasing = True
Const Scaling = True
Const ScalingTo# = 1.1
Const ScalingFrom# = 1 / ScalingTo
;,,,,
; Processing
;----
Graphics 640 , 480 , 0 , 2
TFormFilter AntiAliasing
Local SourceImage
Local SourceAnimStrip
; Single or multi image
SourceAnimStrip = LoadAnimImage ( SourceImageFile ,
SourceImageWidth , SourceImageHeight ,
SourceFrameOffset , SourceFrames )
; Grab image to use from strip
SourceImage = CreateImage ( SourceImageWidth , SourceImageHeight )
SetBuffer ImageBuffer ( SourceImage )
DrawBlock SourceAnimStrip , 0 , 0 , SourceFrameToUse
FreeImage SourceAnimStrip
Local NoMaskImage
; Apply virtual masking
If SourceMask
MaskImage SourceImage , SourceMaskRed , SourceMaskGreen , SourceMaskBlue
; Remove masking color (replace with black)
If RemoveMaskColor
NoMaskImage = CreateImage ( ImageWidth ( SourceImage ) ,
ImageHeight ( SourceImage ) )
SetBuffer ImageBuffer ( NoMaskImage )
DrawImage SourceImage , 0 , 0
FreeImage SourceImage
SourceImage = NoMaskImage
End If
End If
; Centered hotspot
MidHandle SourceImage
; Upsampling (filter dependent)
If Scaling Then ScaleImage SourceImage , ScalingTo , ScalingTo
Local ScreenMidX
Local ScreenMidY
SetBuffer BackBuffer ()
ScreenMidX = GraphicsWidth () / 2
ScreenMidY = GraphicsHeight () / 2
Local RotatedImage
Local Angle
; Rendering
For Angle = 0 To 360 Step RotationInterval
; Make fresh copy of original to be rotated
RotatedImage = CopyImage ( SourceImage )
MidHandle RotatedImage
; Rotate with or without filtering
RotateImage RotatedImage , Angle
; Scale if requested
If Scaling Then ScaleImage RotatedImage , ScalingFrom , ScalingFrom
; Render to screen for user to see progress
DrawImage RotatedImage , ScreenMidX , ScreenMidY
Flip False
; Save image to disk
SaveImage RotatedImage , DestinationFile + Angle + ".BMP"
; Image saved, done
FreeImage RotatedImage
Next
End
;,,,,
;
;----
;,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
Const RotationInterval = 5 ; degrees |
; ( 1-based player number , 0-based angle ) Dim PlayerImages( 2 , 360 ) |
Now, using our handy Image Rotator
we can quickly turn these
into 2 rotated ship image sets.
Then we can use a simple For..Next construct
to load both sets, ready for use!
; Load pre-rotated player images
For PlayerNumber = 1 To 2
For Angle = 0 To 360 Step 5
PlayerImages( PlayerNumber , Angle ) =
LoadImage ( "Ships\" + "Ship " + PlayerNumber + " - " + Angle + ".BMP" )
MidHandle PlayerImages( PlayerNumber , Angle )
Next
Next
|
PlayerAngle = 3 DrawImage PlayerImages( PlayerNumber , PlayerAngle ) , .. |
; Return angle divided in steps
Function LimitAngle ( Angle , Stepsize )
Return Int ( Floor ( Float Angle / Stepsize ) ) * Stepsize
End Function
|
PlayerAngle = 3 DrawImage PlayerImages( PlayerNumber , LimitAngle ( PlayerAngle ) ) , .. |
I have composed a mix of the above parts
and some random bits which I have not explained yet
in Game composition 1.BB,
located in the Tutorial files index,
to give you a good (in-game) look at some of the things
we have dealt with so far.
The example program also includes:
- Some funky debugging features,
to test the gravity effect.
- Sound effects for thrusting and shooting.
- An alternate set of tiles.
- And a new tilemap for testing.
There are more new parts of code
that I will explain in the upcoming chapters.
But if you take a look just at the code structure,
and particularly the sections of that program,
you can see that there's a Player section now.
It contains a bunch of constants,
a type definition, some global variables
and a bunch of functions.
Maybe you have noticed
that this is just like the Bullets section,
which we've handled previously.
; Game speed limiter
Global GameTimer
..
; Setup game speed limiter at 60 updates per second
GameTimer = CreateTimer( 60 )
..
Repeat
..
; Sync game speed
WaitTimer GameTimer
Until ..
|
Graphics 640,480
SetBuffer BackBuffer ()
Local X = 0
Repeat
X = X + 1
If X >= 640
X = 0
End If
Plot X,240
Flip
Cls
Until KeyHit ( 1 )
End
|
Local fps
Local frames
Local lastsec
..
Repeat
..
frames = frames + 1
If MilliSecs () >= lastsec + 1000
lastsec = MilliSecs ()
fps = frames
frames = 0
End If
Color 255 , 255 , 255
Text 0 , 0 , fps
Flip False
Cls
|
; Screen dimensions (for example) Const ScreenWidth = 640 Const ScreenHeight = 480 ; Player 1 (left) Rect 0 , 0 , ScreenWidth/2 , ScreenHeight ; Player 2 (right) Rect ScreenWidth/2 , 0 , ScreenWidth/2 , ScreenHeight |
Type Player
; Current position
Field PosX#
Field PosY#
End Type
|
; ( 1-based player number ) Dim Player.Player( 2 ) ; Create the left player in the upper left corner of the screen Player( 1 ) = New Player Player( 1 )\PosX = 0 Player( 1 )\PosY = 0 ; Create the right player in the upper left corner of the split Player( 2 ) = New Player Player( 2 )\PosX = ScreenWidth/2 Player( 2 )\PosY = 0 |
Function DrawBullets ( Orientation.Player )
; Something with Orientation\PosX and Orientation\PosY
End Function
..
DrawBullets Player( 1 )
DrawBullets Player( 2 )
|
Screen = GameWorld - Camera |
; Player 1 (left view) Rect 0 , 0 , ScreenWidth/2 , ScreenHeight ; View point (left) Plot ScreenWidth/4 , ScreenHeight/2 ; Player 2 (right) Rect ScreenWidth/2 , 0 , ScreenWidth/2 , ScreenHeight ; View point (right) Plot ScreenWidth/2 + ScreenWidth/4 , ScreenHeight/2 |
; Centered in left view Origin ScreenWidth/4 , ScreenHeight/2 Plot 0 , 0 ; Centered in right view Origin ScreenWidth/2 + ScreenWidth/4 , ScreenHeight/2 Plot 0 , 0 |
; Left player view ; - Limit all drawing operations to this area ViewPort 0 , 0 , ScreenWidth/2 , ScreenHeight ; - Offset all drawing operations to this (view) point Origin ScreenWidth/4 , ScreenHeight/2 ; .. Drawing functions ; Right player view ; - Limit all drawing operations to this area ViewPort ScreenWidth/2 , 0 , ScreenWidth/2 , ScreenHeight ; - Offset all drawing operations to this (view) point Origin ScreenWidth/2 + ScreenWidth/4 , ScreenHeight/2 ; .. Drawing functions |
Function DrawBullets ( Orientation.Player )
; Something with Orientation\PosX and Orientation\PosY
|
Function DrawBullets ( OrientationX , OrientationY )
; Something with OrientationX and OrientationY
|
DrawBullets Player( 1 )\PosX , Player( 1 )\PosY |
Screen = GameWorld - Camera |
ScreenBullet\PosX = WorldBullet\PosX - Player\PosX ScreenBullet\PosY = WorldBullet\PosY - Player\PosY |
Function DrawBullets ()
Local This.Bullet
For This = Each Bullet
DrawImage BulletImage , This\PosX , This\PosY
Next
End Function
|
ScreenBullet\PosX = This\PosX - Orientation\PosX ScreenBullet\PosY = This\PosY - Orientation\PosY |
Function DrawBullets ( Orientation.Player )
Local This.Bullet
For This = Each Bullet
DrawImage BulletImage , This\PosX - Orientation\PosX , This\PosY - Orientation\PosY
Next
End Function
|
Function DrawTilemap ( Orientation.Player )
; Something with Orientation\PosX and Orientation\PosY
End Function
..
DrawTilemap Player( 1 )
|
Local PosX Local PosY .. ; Use a relative display position PosX = Orientation\PosX PosY = Orientation\PosY |
Local StartTileX Local StartTileY .. ; Use top-left of player window as offset ; centering the player in the middle of the window ; taking split screen into account ; Then find the tile that's there at this moment ; Add one tile to overlap the window edge StartTileX = ( PosX - ScreenWidth /2/2 ) / TileWidth - 1 StartTileY = ( PosY - ScreenHeight/2 ) / TileHeight - 1 |
Local EndTileX Local EndTileY .. ; Use the player window size as the range ; again centering the player in the middle ; and taking split screen into account ; Then find the tile that's there at this moment ; Add one tile to overlap the window edge EndTileX = ( PosX + ScreenWidth /2/2 ) / TileWidth + 1 EndTileY = ( PosY + ScreenHeight/2 ) / TileHeight + 1 |
To add this to the DrawTilemap function,
simply limit the start and end tile variables
to the tilemap boundaries,
without making any adjustments
to the orientation.
; Make sure that the starting and ending tiles ; are not out of tilemap range (edges) If StartTileX < 0 Then StartTileX = 0 If StartTileY < 0 Then StartTileY = 0 If EndTileX > TilemapWidth -1 Then EndTileX = TilemapWidth -1 If EndTileY > TilemapHeight-1 Then EndTileY = TilemapHeight-1 |
Local TileX
Local TileY
Local TilePosX
Local TilePosY
Local FrameNumber
..
; Now go through only those tiles that are within the player's window
For TileX = StartTileX To EndTileX
For TileY = StartTileY To EndTileY
TilePosX = TileX * TileWidth - PosX
TilePosY = TileY * TileHeight - PosY
FrameNumber = Tilemap( TileX+1 , TileY+1 )
DrawImage TileStrip , TilePosX , TilePosY , FrameNumber
Next
Next
|
FrameNumber = Tilemap( TileX+1 , TileY+1 )
; Is non-zero
If FrameNumber
TilePosX = TileX * TileWidth - PosX
TilePosY = TileY * TileHeight - PosY
DrawImage TileStrip , TilePosX , TilePosY , FrameNumber
End If
|
Function DrawTilemap ( Orientation.Player )
Local StartTileX
Local StartTileY
Local EndTileX
Local EndTileY
Local TileX
Local TileY
Local TilePosX
Local TilePosY
Local FrameNumber
Local PosX
Local PosY
; Use a relative display position
PosX = Orientation\PosX
PosY = Orientation\PosY
; Use top-left of player window as offset
; centering the player in the middle of the window
; taking split screen into account
; Then find the tile that's there at this moment
; Add one tile to overlap the window edge
StartTileX = ( PosX - ScreenWidth /2/2 ) / TileWidth - 1
StartTileY = ( PosY - ScreenHeight/2 ) / TileHeight - 1
; Use the player window size as the range
; again centering the player in the middle
; and taking split screen into account
; Then find the tile that's there at this moment
; Add one tile to overlap the window edge
EndTileX = ( PosX + ScreenWidth /2/2 ) / TileWidth + 1
EndTileY = ( PosY + ScreenHeight/2 ) / TileHeight + 1
; Make sure that the starting and ending tiles
; are not out of tilemap range (edges)
If StartTileX < 0 Then StartTileX = 0
If StartTileY < 0 Then StartTileY = 0
If EndTileX > TilemapWidth -1 Then EndTileX = TilemapWidth -1
If EndTileY > TilemapHeight-1 Then EndTileY = TilemapHeight-1
; Now go through only those tiles that are within the player's window
For TileX = StartTileX To EndTileX
For TileY = StartTileY To EndTileY
FrameNumber = Tilemap( TileX+1 , TileY+1 )
; Is non-zero
If FrameNumber
TilePosX = TileX * TileWidth - PosX
TilePosY = TileY * TileHeight - PosY
DrawImage TileStrip , TilePosX , TilePosY , FrameNumber
End If
Next
Next
End Function
|
Type Player
..
; Current ship angle
Field Angle#
..
End Type
|
So let's add a DrawPlayers function
which combines that technique
as well as the camera viewpoint.
Function DrawPlayers ( Main.Player )
Local This.Player
Local PosX
Local PosY
For This = Each Player
; But it's not the player being drawn
If This <> Main
; Make this player relative to the one being drawn
PosX = This\PosX - Main\PosX
PosY = This\PosY - Main\PosY
; This player is being drawn
Else
; Center it
PosX = 0
PosY = 0
End If
; Limit the angle to increments of 5, starting from 0
; to match with the image frames stored in the array
DrawImage PlayerImages( This\Number , LimitAngle ( This\Angle , 5 ) ) , PosX , PosY
Next
End Function
|
Type Player
..
; Unique player number (1 or 2)
Field Number
..
End Type
|
; Render game ; - Player 1 ViewPort 0 , 0 , ScreenWidth / 2 , ScreenHeight Origin ScreenWidth / 4 , ScreenHeight / 2 DrawTilemap Player( 1 ) DrawBullets Player( 1 ) DrawPlayers Player( 1 ) ; - Player 2 ViewPort ScreenWidth / 2 , 0 , ScreenWidth / 2 , ScreenHeight Origin ScreenWidth / 2 + ScreenWidth / 4 , ScreenHeight / 2 DrawTilemap Player( 2 ) DrawBullets Player( 2 ) DrawPlayers Player( 2 ) |
Function CreatePlayer.Player ( PlayerNumber )
Local This.Player
This = New Player
This\Number = PlayerNumber
; Center view on tilemap by default
This\PosX = (TilemapWidth -1) * TileWidth / 2
This\PosY = (TilemapHeight-1) * TileHeight / 2
Return This
End Function
..
; Setup players
Player( 1 ) = CreatePlayer ( 1 )
Player( 2 ) = CreatePlayer ( 2 )
|
; ( 1-based player number ) Dim Player.Player( 2 ) |
Type Player
; Unique player number (1 or 2)
Field Number
; Current ship angle
Field Angle#
; Current ship heading (direction)
Field Heading#
; Current ship speed (velocity)
Field Speed#
; Current position
Field PosX#
Field PosY#
End Type
|
Type Player
..
; True or False
Field Dead
..
End Type
..
Function CreatePlayer.Player ( PlayerNumber )
..
This\Dead = True
|
Function RespawnPlayer ( This.Player )
Local Here.SpawnSpot
Local Channel
; Find a random spawn spot
Here = RandomSpawnSpot ()
; Convert tilemap scale (array) to pixel scale (world)
This\PosX = (Here\PosX-1) * TileWidth
This\PosY = (Here\PosY-1) * TileHeight
; Point up and reset speed
This\Angle = 0
This\Speed = 0
This\Heading = 0
; Make alive and kicking
This\Dead = False
; Play respawn sound
Channel = PlaySample ( SpawnSound , 10 )
ChannelPan Channel , ( This\Number - 1 ) * 2 - 1
End Function
|
; Effects Global SpawnSound .. ; Load effects SpawnSound = LoadSound ( "Spawn.WAV" ) |
; Grab a random spawnspot from the list
Function RandomSpawnSpot.SpawnSpot ()
Local Number
Local Times
Local This.SpawnSpot
; Start at the front
This = First SpawnSpot
; Pick a random one using the known number of spawn spots
Times = Rand ( 0 , SpawnSpots - 1 )
; Count until reached target value
For Number = 1 To Times
; Next spawn spot
This = After This
Next
Return This
End Function
|
; Reset randomizer ; (set number of milliseconds since system boot as randomizer base value) SeedRnd MilliSecs () |
; Player 1 (left)
Const KeyW = 17
Const KeyA = 30
Const KeyS = 31
Const KeyD = 32
; Player 2 (right)
Const KeyUp = 200
Const KeyDown = 208
Const KeyLeft = 203
Const KeyRight = 205
..
Function PlayersInput ()
Local Player.Player
Local Thrust
Local RotateLeft
Local RotateRight
Local Fire
For Player = Each Player
Select Player\Number
Case 1: Thrust = KeyS
RotateLeft = KeyA
RotateRight = KeyD
Fire = KeyW
Case 2: Thrust = KeyDown
RotateLeft = KeyLeft
RotateRight = KeyRight
Fire = KeyUp
End Select
If KeyDown ( Thrust ) Then RespawnPlayer Player
If KeyDown ( RotateLeft ) Then RotatePlayerLeft Player
If KeyDown ( RotateRight ) Then RotatePlayerRight Player
If KeyHit ( Fire ) Then FirePlayer Player
Next
End Function
|
; Control both ships PlayersInput |
Global ThrustSound .. ThrustSound = LoadSound ( "Thrust.WAV" ) LoopSound ThrustSound |
Type Player
..
; Request to thrust ship
Field Thrusting
; Sound channel handle
Field ThrustChannel
..
End Type
|
; Prepare thrust sound This\ThrustChannel = PlaySound ( ThrustSound ) PauseChannel This\ThrustChannel ChannelPan This\ThrustChannel , ( PlayerNumber - 1 ) * 2 - 1 |
If KeyDown ( Thrust )
If Player\Dead
RespawnPlayer Player
Else
Player\Thrusting = True
; Play thrust sound
ResumeChannel Player\ThrustChannel
End If
Else
; Stop thrust sound
PauseChannel Player\ThrustChannel
End If
|
; Player (ship) rotation speed in degrees
Const RotateSpeed# = 4
..
Function RotatePlayerLeft ( This.Player )
If Not This\Dead
This\Angle = This\Angle - RotateSpeed
If This\Angle < 0 Then This\Angle = This\Angle + 360
End If
End Function
..
Function RotatePlayerRight ( This.Player )
If Not This\Dead
This\Angle = This\Angle + RotateSpeed
If This\Angle >= 360 Then This\Angle = This\Angle - 360
End If
End Function
|
The algorithm to calculate all of this
is almost identical to the one
used in Project Z: Two.
We can use the code
from the Bullets chapter
with only a few changes.
Global FireSound
..
FireSound = LoadSound ( "Fire.WAV" )
..
Function FirePlayer ( This.Player )
Local PosX
Local PosY
Local VelX#
Local VelY#
Local Channel
If Not This\Dead
; Calculate where to create bullet in front of ship
PosX = This\PosX + VectorX ( BulletDistance , This\Angle )
PosY = This\PosY + VectorY ( BulletDistance , This\Angle )
; Initial bullet velocity vector
VelX = VectorX ( BulletSpeed , This\Angle )
VelY = VectorY ( BulletSpeed , This\Angle )
; Add player velocity vector to bullet velocity vector (realism)
VelX = VelX + VectorX ( This\Speed , This\Heading )
VelY = VelY + VectorY ( This\Speed , This\Heading )
CreateBullet PosX , PosY , VelX , VelY
; Play fire sound
Channel = PlaySample ( FireSound , 10 )
ChannelPan Channel , ( This\Number - 1 ) * 2 - 1
End If
End Function
|
..
For This = Each Player
; Player is dead
If Not This\Dead
..
|
Global ExplodeSound
..
ExplodeSound = LoadSound ( "Explode.WAV" )
..
Function KillPlayer ( This.Player )
Local Channel
This\Dead = True
; Stop thrust sound
PauseChannel This\ThrustChannel
; Create a bunch of shrapnel pieces in random directions
CreateBulletRay This\PosX , This\PosY
; Play explode sound
Channel = PlaySample ( ExplodeSound , 50 )
ChannelPan Channel , ( This\Number - 1 ) * 2 - 1
End Function
|
Function CreateBulletRay ( InitialPosX , InitialPosY , InitialVelX# = 0 , InitialVelY# = 0 )
Local Angle
Local RandomAngle
Local VelX#
Local VelY#
Local PosX#
Local PosY#
; Divide 360 degrees in sections of 5 degrees
For Angle = 0 To 360 Step 360/5
; Then choose a random angle inside that peace of pie
RandomAngle = Angle + Rand ( 360/5 )
; Velocity: use half of bullet speed plus the initial speed (optional)
VelX = InitialVelX + VectorX ( BulletSpeed / 2 , RandomAngle )
VelY = InitialVelY + VectorY ( BulletSpeed / 2 , RandomAngle )
; Position: use half of ship tip distance plus the offset position
PosX = InitialPosX + VectorX ( BulletDistance / 2 , RandomAngle )
PosY = InitialPosY + VectorY ( BulletDistance / 2 , RandomAngle )
; Finally create it, but it's not a normal bullet
CreateBullet PosX , PosY , VelX , VelY , False
Next
End Function
|
Function CreateBullet ( PosX , PosY , VelX# , VelY# , NormalBullet = True )
..
This\TimeCreated = MilliSecs
; Bullet
If NormalBullet
; Use default time to live
This\TimeToLive = BulletTime
; Shrapnel
Else
; Use slightly randomized default time to live
This\TimeToLive = BulletTime / 2 + Rand ( BulletTime )
End If
End Function
|
; Player (ship) thrust speed in pixels
Const ThrustSpeed# = 0.12
; Player (ship) maximum velocity in pixels
Const MaxSpeed = 5
; Downward gravity strength in pixels (on ship)
Const GravityPull# = 0.03
..
Function UpdatePlayer ( This.Player )
Local ThrustX#
Local ThrustY#
Local SpeedX#
Local SpeedY#
If Not This\Dead
If This\Thrusting
This\Thrusting = False
; Thrust vector
ThrustX = VectorX ( ThrustSpeed , This\Angle )
ThrustY = VectorY ( ThrustSpeed , This\Angle )
Else
ThrustX = 0
ThrustY = 0
End If
; Thrust vector + old velocity vector = new velocity vector
SpeedX = VectorX ( This\Speed , This\Heading ) + ThrustX
SpeedY = VectorY ( This\Speed , This\Heading ) + ThrustY
; Add gravity vector to velocity vector (180 degrees = down)
SpeedX = SpeedX + VectorX ( GravityPull , 180 )
SpeedY = SpeedY + VectorY ( GravityPull , 180 )
; Update speed and heading with new velocity vector
This\Speed = VectorDistance ( SpeedX , SpeedY )
This\Heading = VectorAngle ( SpeedX , SpeedY )
; Limit velocity (speed of ship)
If This\Speed > MaxSpeed
This\Speed = MaxSpeed
; Recalculate limited velocity vector
SpeedX = VectorX ( This\Speed , This\Heading )
SpeedY = VectorY ( This\Speed , This\Heading )
End If
; Update position with velocity vector
This\PosX = This\PosX + SpeedX
This\PosY = This\PosY + SpeedY
End If
End Function
|
; Update game UpdatePlayer Player( 1 ) UpdatePlayer Player( 2 ) UpdateBullets |
; Soundtrack Global GameMusic ; Background music channel handle Global MusicChannel .. ; Load and play music GameMusic = LoadSound ( "TheChance - Gravity Force.MP3" ) LoopSound GameMusic MusicChannel = PlaySound ( GameMusic ) ; - At half volume, to put it in the background ChannelVolume MusicChannel , 0.5 |
; Locations of external files Const AudioFolder$ = "Audio\" Const GraphicsFolder$ = "Graphics\" Const TilemapsFolder$ = "Tilemaps\" |
GameMusic = LoadSound ( AudioFolder + "TheChance - Gravity Force.MP3" ) |
PlayerImages( PlayerNumber , Angle ) =
LoadImage ( GraphicsFolder + "Ships\" + "Ship " + PlayerNumber + " - " + Angle + ".BMP" )
|
; Both players must be alive If ( Not Player( 1 )\Dead ) And ( Not Player( 2 )\Dead ) |
Local PlayerImg1 Local PlayerImg2 .. PlayerImg1 = PlayerImages( 1 , LimitAngle ( Player( 1 )\Angle , 5 ) ) PlayerImg2 = PlayerImages( 2 , LimitAngle ( Player( 2 )\Angle , 5 ) ) |
If ImagesOverlap ( PlayerImg1 , Player( 1 )\PosX , Player( 1 )\PosY ,
PlayerImg2 , Player( 2 )\PosX , Player( 2 )\PosY )
|
If ImagesCollide ( PlayerImg1 , Player( 1 )\PosX , Player( 1 )\PosY , 0 ,
PlayerImg2 , Player( 2 )\PosX , Player( 2 )\PosY , 0 )
|
; And kill them if so KillPlayer Player( 1 ) KillPlayer Player( 2 ) |
Function PlayerCollideWithPlayer ()
Local PlayerImg1
Local PlayerImg2
; Both players must be alive
If ( Not Player( 1 )\Dead ) And ( Not Player( 2 )\Dead )
PlayerImg1 = PlayerImages( 1 , LimitAngle ( Player( 1 )\Angle , 5 ) )
PlayerImg2 = PlayerImages( 2 , LimitAngle ( Player( 2 )\Angle , 5 ) )
If ImagesOverlap ( PlayerImg1 , Player( 1 )\PosX , Player( 1 )\PosY ,
PlayerImg2 , Player( 2 )\PosX , Player( 2 )\PosY )
If ImagesCollide ( PlayerImg1 , Player( 1 )\PosX , Player( 1 )\PosY , 0 ,
PlayerImg2 , Player( 2 )\PosX , Player( 2 )\PosY , 0 )
; And kill them if so
KillPlayer Player( 1 )
KillPlayer Player( 2 )
End If
End If
End If
End Function
|
Local Player.Player
Local PlayerImg
..
For Player = Each Player
If Not Player\Dead
PlayerImg = PlayerImages( Player\Number , LimitAngle ( Player\Angle , 5 ) )
|
Because now I'm adding a similar loop for each bullet
right below the previous code,
directly followed by the image collision checks,
just like in the ship to ship collision routine.
Only now, using the BulletImage.
Local This.Bullet
..
For This = Each Bullet
If ImagesOverlap ( PlayerImg , Player\PosX , Player\PosY ,
BulletImage , This\PosX , This\PosY )
If ImagesCollide ( PlayerImg , Player\PosX , Player\PosY , 0 ,
BulletImage , This\PosX , This\PosY , 0 )
|
Delete This KillPlayer Player ; This player is dead, don't check for any more bullets Exit |
Function PlayersCollideWithBullets ()
Local Player.Player
Local This.Bullet
Local PlayerImg
For Player = Each Player
If Not Player\Dead
PlayerImg = PlayerImages( Player\Number , LimitAngle ( Player\Angle , 5 ) )
For This = Each Bullet
If ImagesOverlap ( PlayerImg , Player\PosX , Player\PosY ,
BulletImage , This\PosX , This\PosY )
If ImagesCollide ( PlayerImg , Player\PosX , Player\PosY , 0 ,
BulletImage , This\PosX , This\PosY , 0 )
Delete This
KillPlayer Player
; This player is dead, don't check for any more bullets
Exit
End If
End If
Next
End If
Next
End Function
|
Local Player.Player
Local PlayerImg
..
For Player = Each Player
If Not Player\Dead
PlayerImg = PlayerImages( Player\Number , LimitAngle ( Player\Angle , 5 ) )
|
Local TileX Local TileY Local StartTileX Local StartTileY Local EndTileX Local EndTileY .. ; Tile under player TileX = Player\PosX / TileWidth TileY = Player\PosY / TileHeight ; 3x3 tile block under player StartTileX = TileX - 1 StartTileY = TileY - 1 EndTileX = TileX + 1 EndTileY = TileY + 1 |
; Limit to edges of tilemap If StartTileX < 0 Then StartTileX = 0 If StartTileY < 0 Then StartTileY = 0 If EndTileX > TilemapWidth -1 Then EndTileX = TilemapWidth -1 If EndTileY > TilemapHeight-1 Then EndTileY = TilemapHeight-1 |
For TileX = StartTileX To EndTileX
For TileY = StartTileY To EndTileY
; Is non-zero
If Tilemap( TileX+1 , TileY+1 )
|
Local TilePosX
Local TilePosY
..
TilePosX = TileX * TileWidth
TilePosY = TileY * TileHeight
If ImagesOverlap ( PlayerImg , Player\PosX , Player\PosY ,
TileStrip , TilePosX , TilePosY )
If ImagesCollide ( PlayerImg , Player\PosX , Player\PosY , 0 ,
TileStrip , TilePosX , TilePosY , Tilemap( TileX+1 , TileY+1 ) )
|
KillPlayer Player |
Function PlayersCollideWithTilemap ()
Local Player.Player
Local TileX
Local TileY
Local StartTileX
Local StartTileY
Local EndTileX
Local EndTileY
Local TilePosX
Local TilePosY
Local PlayerImg
For Player = Each Player
If Not Player\Dead
PlayerImg = PlayerImages( Player\Number , LimitAngle ( Player\Angle , 5 ) )
; Tile under player
TileX = Player\PosX / TileWidth
TileY = Player\PosY / TileHeight
; 3x3 tile block under player
StartTileX = TileX - 1
StartTileY = TileY - 1
EndTileX = TileX + 1
EndTileY = TileY + 1
; Limit to edges of tilemap
If StartTileX < 0 Then StartTileX = 0
If StartTileY < 0 Then StartTileY = 0
If EndTileX > TilemapWidth -1 Then EndTileX = TilemapWidth -1
If EndTileY > TilemapHeight-1 Then EndTileY = TilemapHeight-1
For TileX = StartTileX To EndTileX
For TileY = StartTileY To EndTileY
; Is non-zero
If Tilemap( TileX+1 , TileY+1 )
TilePosX = TileX * TileWidth
TilePosY = TileY * TileHeight
If ImagesOverlap ( PlayerImg , Player\PosX , Player\PosY ,
TileStrip , TilePosX , TilePosY )
If ImagesCollide ( PlayerImg , Player\PosX , Player\PosY , 0 ,
TileStrip , TilePosX , TilePosY , Tilemap( TileX+1 , TileY+1 ) )
KillPlayer Player
End If
End If
End If
Next
Next
End If
Next
End Function
|
Local This.Bullet
..
For This = Each Bullet
TileX = This\PosX / TileWidth
TileY = This\PosY / TileHeight
..
If ImagesOverlap ( BulletImage , This\PosX , This\PosY ,
TileStrip , TilePosX , TilePosY )
If ImagesCollide ( BulletImage , This\PosX , This\PosY , 0 ,
TileStrip , TilePosX , TilePosY , Tilemap( TileX+1 , TileY+1 ) )
Delete This
|
Local ExitLoops |
ExitLoops = False |
ExitLoops = True Exit |
Next ; TileY
If ExitLoops Then Exit
Next ; TileX
|
; Play collide sound PlaySample CollideSound , 50 |
Global CollideSound |
CollideSound = LoadSound ( AudioFolder + "Collide.WAV" ) |
Function BulletsCollideWithTilemap ()
Local This.Bullet
Local TileX
Local TileY
Local StartTileX
Local StartTileY
Local EndTileX
Local EndTileY
Local TilePosX
Local TilePosY
Local ExitLoops
For This = Each Bullet
TileX = This\PosX / TileWidth
TileY = This\PosY / TileHeight
StartTileX = TileX - 1
StartTileY = TileY - 1
EndTileX = TileX + 1
EndTileY = TileY + 1
If StartTileX < 0 Then StartTileX = 0
If StartTileY < 0 Then StartTileY = 0
If EndTileX > TilemapWidth -1 Then EndTileX = TilemapWidth -1
If EndTileY > TilemapHeight-1 Then EndTileY = TilemapHeight-1
ExitLoops = False
For TileX = StartTileX To EndTileX
For TileY = StartTileY To EndTileY
; Is non-zero
If Tilemap( TileX+1 , TileY+1 )
TilePosX = TileX * TileWidth
TilePosY = TileY * TileHeight
If ImagesOverlap ( BulletImage , This\PosX , This\PosY ,
TileStrip , TilePosX , TilePosY )
If ImagesCollide ( BulletImage , This\PosX , This\PosY , 0 ,
TileStrip , TilePosX , TilePosY , Tilemap( TileX+1 , TileY+1 ) )
Delete This
; Play collide sound
PlaySample CollideSound , 50
ExitLoops = True
Exit
End If
End If
End If
Next
If ExitLoops Then Exit
Next
Next
End Function
|
; Collisions PlayerCollideWithPlayer PlayersCollideWithTilemap PlayersCollideWithBullets BulletsCollideWithTilemap |
Followed by the value's initialization
to be placed in the Main initialization section.
; Game states Global GamePaused .. ; Set default game states GamePaused = False |
; Game control Const KeyP = 25 Const KeyEsc = 1 |
If KeyHit ( KeyP )
GamePaused = Not GamePaused
End If
..
Until KeyHit ( KeyEsc )
|
If Not GamePaused
; Control both ships
PlayersInput
; Update game
UpdatePlayer Player( 1 )
UpdatePlayer Player( 2 )
UpdateBullets
; Collisions
PlayerCollideWithPlayer
PlayersCollideWithTilemap
PlayersCollideWithBullets
BulletsCollideWithTilemap
End If
|
; Main loop
Repeat
; Sync time
MilliSecs = MilliSecs ()
If KeyHit ( KeyP )
GamePaused = Not GamePaused
End If
If Not GamePaused
; Control both ships
PlayersInput
; Update game
UpdatePlayer Player( 1 )
UpdatePlayer Player( 2 )
UpdateBullets
; Collisions
PlayerCollideWithPlayer
PlayersCollideWithTilemap
PlayersCollideWithBullets
BulletsCollideWithTilemap
End If
; Render game
; - Player 1
ViewPort 0 , 0 , ScreenWidth / 2 , ScreenHeight
Origin ScreenWidth / 4 , ScreenHeight / 2
DrawTilemap Player( 1 )
DrawBullets Player( 1 )
DrawPlayers Player( 1 )
; - Player 2
ViewPort ScreenWidth / 2 , 0 , ScreenWidth / 2 , ScreenHeight
Origin ScreenWidth / 2 + ScreenWidth / 4 , ScreenHeight / 2
DrawTilemap Player( 2 )
DrawBullets Player( 2 )
DrawPlayers Player( 2 )
; - Screen
Viewport 0 , 0 , ScreenWidth , ScreenHeight
Origin 0 , 0
Color 0 , 0 , 0
Rect ScreenWidth/2 , 0 , 1 , ScreenHeight
Flip
Cls
; Sync game speed
WaitTimer GameTimer
Until KeyHit ( KeyEsc )
; Blitz cleans up
End
|
Time related
Input related
Objects related
Rendering related
Here are some examples
for each of these.
Time related examples:
- Set MilliSecs to MilliSecs().
- Calculate the number of Frames Per Second.
Input related examples:
- Call PlayersInput().
- Pause or unpause the game.
Object related examples:
- Update the player ships.
- Handle physics.
- Collision detection.
Rendering related examples:
- Set viewports.
- Draw backgrounds and objects.
Sometimes parts that are rendering related
can also be time related,
like WaitTimer in the main loop.
So for me it makes sense
because from a run-time point of view
the time related section is executed
right after the rendering related section.
Feel free to experiment!
).
The 'pause game'-bug
When you fly around shooting,
and suddenly pause the game,
wait for a little bit,
and unpause the game again,
bullets will be gone instantly.
This is because MilliSecs()
is not paused when the game is paused.
This can be fixed for example
by adding the duration of the Pause
to the value of the TimeCreated field
in every Bullet object.
Global PauseStart
..
If KeyHit ( KeyP )
If Not GamePaused
PauseStart = MilliSecs
Else
BulletTime MilliSecs - PauseStart
End If
GamePaused = Not GamePaused
End If
..
Function BulletTime ( TimeShift )
Local This.Bullet
For This = Each Bullet
This\TimeCreated = This\TimeCreated + TimeShift
Next
End Function
|
Input buffer bugs
Sometimes when you press keys (like Esc)
while the game is being paused,
it will cause havoc (like instant quit)
once the game is unpaused.
I like fixing these particular bugs
by placing a FlushKeys
right after unpausing the game.
If KeyHit ( KeyP )
GamePaused = Not GamePaused
FlushKeys ; Forget pressed keys
End If
|
; Clear any pressed keys FlushKeys |
Superfast respawn
When your ship crashes,
you can instantly press the Thrust/Respawn key again
to instantly respawn at a new spawn spot.
I feel this is a little too flexible for this game,
especially in the middle of a dogfight!
So I've decided to add some code to prevent
a player from respawning its ship
for a certain amount of time
after having crashed.
So this would affect
the RespawnPlayer
and KillPlayer functions.
Similar to a PauseStart global variable
I'll need to keep track of the time
the player crashed.
But here,
both players can crash,
so I'll add another field
to the Player object.
Type Player
..
; Time of death
Field DiedWhen
|
Function KillPlayer ( This.Player )
..
This\DiedWhen = MilliSecs
|
Function RespawnPlayer ( This.Player )
..
; How much time to wait (in ms) before being able to spawn after dying
If MilliSecs - This\DiedWhen >= 2000
|
And could also have been placed
in the Definition area of the code
using a Constant.
Specifically these 2 seconds allow
for the 'forced' display
of the nice explosion effect
when a ship crashes.
Spawn-crashing
Imagine both players spawning simultaneously.
The game will spawn both players regardless,
and at a completely random spawn spot.
This means that sometimes
you can spawn exactly where the other player already is!
One way to fix this is for example
by remembering the last spawn spot used,
or creating a list of randomized spawn spots.
But I've chosen a different approach,
which also works when having just 1 spawn spot.
I'm going to check for a collision
before actually spawning the player.
This means that I would have to put
a call to PlayerCollideWithPlayer
inside the RespawnPlayer function (!).
PlayerCollideWithPlayer currently takes into account
the occurence of both players being dead already,
so I have to modify it first.
But it is already being used
for normal collision detection
in the main loop.
So I will duplicate the function,
while being able to re-use existing code
and keeping full backward-compatibility
with the current collision detection.
Function PlayerCollideWithPlayer ()
; Both players must be alive
If ( Not Player( 1 )\Dead ) And ( Not Player( 2 )\Dead )
; Then see if they collide
If PlayersCollide ()
; And kill them if so
KillPlayer Player( 1 )
KillPlayer Player( 2 )
End If
End If
End Function
|
Function PlayersCollide ()
Local PlayerImg1
Local PlayerImg2
PlayerImg1 = PlayerImages( 1 , LimitAngle ( Player( 1 )\Angle , 5 ) )
PlayerImg2 = PlayerImages( 2 , LimitAngle ( Player( 2 )\Angle , 5 ) )
If ImagesOverlap ( PlayerImg1 , Player( 1 )\PosX , Player( 1 )\PosY ,
PlayerImg2 , Player( 2 )\PosX , Player( 2 )\PosY )
If ImagesCollide ( PlayerImg1 , Player( 1 )\PosX , Player( 1 )\PosY , 0 ,
PlayerImg2 , Player( 2 )\PosX , Player( 2 )\PosY , 0 )
Return True
End If
End If
End Function
|
Function RespawnPlayer ( This.Player )
..
Local Respawn
..
; Prevent spawning over another player, if a player is alive
If ( Not Player( 1 )\Dead ) Or ( Not Player( 2 )\Dead )
If Not PlayersCollide ()
Respawn = True
End If
Else
Respawn = True
End If
|
If Respawn
; Make alive and kicking
This\Dead = False
|
; Stars in the background Global BackgroundImage .. ; Load background image BackgroundImage = LoadImage ( GraphicsFolder + "Star Background.PNG" ) |
; - Screen
Viewport 0 , 0 , ScreenWidth , ScreenHeight
Origin 0 , 0
Color 0 , 0 , 0
Rect ScreenWidth/2 , 0 , 1 , ScreenHeight
Flip
TileBlock
|
TileBlock BackgroundImage , -Player( 1 )\PosX/2 , -Player( 1 )\PosY/2 |
; Render game ; - Player 1 Viewport 0 , 0 , ScreenWidth / 2 , ScreenHeight TileBlock BackgroundImage , -Player( 1 )\PosX/2 , -Player( 1 )\PosY/2 Origin ScreenWidth / 4 , ScreenHeight / 2 DrawTilemap Player( 1 ) DrawBullets Player( 1 ) DrawPlayers Player( 1 ) ; - Player 2 Viewport ScreenWidth / 2 , 0 , ScreenWidth / 2 , ScreenHeight TileBlock BackgroundImage , -Player( 2 )\PosX/2 , -Player( 2 )\PosY/2 Origin ScreenWidth / 2 + ScreenWidth / 4 , ScreenHeight / 2 DrawTilemap Player( 2 ) DrawBullets Player( 2 ) DrawPlayers Player( 2 ) |
; Resolution constants from Blitz
Const WindowMode_AutoDetect = 0
Const WindowMode_FullScreen = 1
Const WindowMode_Windowed = 2
Const WindowMode_Scaled = 3
..
; Graphics resolution
Global ScreenWidth
Global ScreenHeight
Global WindowMode
Global ColorDepth
..
; Check if a resolution is listed in the video card's supported videomodes
Function ResAvail ( Width , Height )
Local ColorDepth
For ColorDepth = 16 To 32 Step 8
If GfxModeExists ( Width , Height , ColorDepth )
Return ColorDepth
End If
Next
End Function
..
Function InitGame ()
..
; Windowed in debug mode, otherwise fullscreen
WindowMode = WindowMode_AutoDetect
; Autodetect optimal resolution
; - Wide screen medium resolution (2.13x wide)
ScreenWidth = 1024
ScreenHeight = 480
ColorDepth = ResAvail ( ScreenWidth , ScreenHeight )
If Not ColorDepth
; - Wide screen high resolution (1.78x wide)
ScreenWidth = 1280
ScreenHeight = 720
ColorDepth = ResAvail ( ScreenWidth , ScreenHeight )
If Not ColorDepth
; - Wide screen medium-low resolution (1.77x wide)
ScreenWidth = 848
ScreenHeight = 480
ColorDepth = ResAvail ( ScreenWidth , ScreenHeight )
If Not ColorDepth
; - Wide screen low resolution (1.5x wide)
ScreenWidth = 720
ScreenHeight = 480
ColorDepth = ResAvail ( ScreenWidth , ScreenHeight )
If Not ColorDepth
; - CRT screen high resolution (1.33x wide)
ScreenWidth = 1024
ScreenHeight = 768
ColorDepth = ResAvail ( ScreenWidth , ScreenHeight )
If Not ColorDepth
; - CRT screen medium resolution (1.33x wide)
ScreenWidth = 800
ScreenHeight = 600
ColorDepth = ResAvail ( ScreenWidth , ScreenHeight )
If Not ColorDepth
; - CRT screen low resolution (1.33x wide)
ScreenWidth = 640
ScreenHeight = 480
ColorDepth = ResAvail ( ScreenWidth , ScreenHeight )
; - If this resolution will not work either,
; Blitz will show an error message anyway
End If
End If
End If
End If
End If
End If
; Try the detected screen resolution
Graphics ScreenWidth , ScreenHeight , ColorDepth , WindowMode
..
|
Function Verify ( TargetHandle , ErrorMessage$ = "probs :(" )
If Not TargetHandle Then RuntimeError ErrorMessage
End Function
|
; Start music first GameMusic = LoadSound ( AudioFolder + "TheChance - Gravity Force.MP3" ) Verify GameMusic , "Unable to load intro music" |
Local PlayerNumber
Local Angle
..
; Load pre-rotated player images
For PlayerNumber = 1 To 2
For Angle = 0 To 360 Step 5
PlayerImages( PlayerNumber , Angle ) =
LoadImage ( GraphicsFolder + "Ships\" +
"Ship " + PlayerNumber + " - " + Angle + ".BMP" )
Verify PlayerImages( PlayerNumber , Angle ) ,
"Unable to load ship image " + PlayerNumber + "," + Angle
|
Local TilemapLoaded .. ; Load tile map data TilemapLoaded = LoadTilemap ( TilemapsFolder + "Test Tilemap 1.TXT" ) Verify TilemapLoaded , "Unable to load tilemap" |
Function LoadTilemap ( FileName$ )
..
CloseFile FileHandle
; Return true if any spawn spots found, otherwise false
If SpawnSpots > 0
Return True
Else
Return False
End If
End Function
|
; Load tile map data Verify LoadTilemap ( TilemapsFolder + "Test Tilemap 1.TXT" ) , "Unable to load tilemap" .. ; Return true if any spawn spots found, otherwise false Return SpawnSpots > 0 |
Note that there are a few more ways
to crash the game
For example by altering the tilemap file
and putting in some bogus data
But I'll leave it up to you to figure out
how to prevent the game from crashing then.
For example you can range-check the values
like TilemapWidth and TilemapHeight
after having been loaded in with ReadLine.
Local IntroImage .. ; Show splash screen / front-end IntroImage = LoadImage ( GraphicsFolder + "X-NON Logo.PNG" ) Verify IntroImage , "Unable to load intro image" MidHandle IntroImage DrawImage IntroImage , ScreenWidth / 2 , ScreenHeight / 2 |
; Remember when started loading LoadStart = MilliSecs () |
; If loading took less than 3 seconds, ; wait until those same 3 seconds are up. ; This, in order to make the game logo visible ; for at least a little while. While MilliSecs () - LoadStart < 3000 Wend |
ASCII Converter.BB
This allows you to convert images to ASCII files and back,
so you can edit tilemaps using drawing programs!
Editor (simple).BB
The tilemap file you want to work on
is specified directly in the code itself.
It is loaded when the program is run,
and also saved when the you quit the program.
These are the keys you can use in the program:
Space: Toggle transparent drawing tile
Cursor keys: Move map
(+Shift): Move map faster
Enter: Select tile for drawing
Left mouse button: Draw selected tile
Right mouse button: Erase tile
Middle mouse button: Pickup tile from tilemap
Escape: Exit and save
Editor (elaborate).BB
This program works as a 'normal' tilemap editor should
by being able to load and save from within the program.
Inside the code you can easily change the tileset to use,
for example the tileset for the tilemap or the logo.
Keyboard shortcuts:
Tab: Show grid overlay
Space: Toggle drawing tile transparency and hide empty tiles
N: New tilemap (width,height)
L: Load tilemap (filename)
S: Save tilemap (filename)
Cursor keys: Move tilemap
(+Shift): Move tilemap faster
Enter: Select tile to draw
Keypad cursor keys: Move tile selection
Left mouse button: Paint tile
Right mouse button: Erase tile
Middle mouse button: Pickup tile
Escape: Exit
Here are some more features
that I thought would be interesting
to be added to the game
at a later stage:
- Ship health;
making the ship flash when hit,
needing more than 1 hit to be killed.
- A simple scoring system;
so you don't have to count yourself
- A simple menu interface;
so you can change some settings,
like speed, gravity, controls, etc.
- New tilemaps;
for some more variation.
- New tiles with more spice;
to make the experience more vivid.
- New ships;
allowing players to choose a ship
that fits their own style.
You can also go overboard
and pick some of these:
- Tilemap editor integrated into the game;
so you can add or modify maps with ease.
- Single player missions;
if you don't have a partner to play with,
you can fly through a tilemap maze
with various obstacles to overcome.
- Network play (like DirectPlay);
to play with anyone you like,
without leaving your home.
You see, I sometimes have the tendency
to make things more complex than they have to be.
So I immensely simplified the game structure a "few" times.
If you're interested,
the full blown project package is available,
containing all of these layers of simplification,
as well as all stages of construction (and rewrites),
weighing at ~50 Megabytes.
I've included one of the most useful bits
from the full blown project package
combined in a single bonus tutorial file,
which is called Plan Snippets.txt
located in the Extra folder.
It contains considerable background information
and even more details behind the construction of X-NON
using a home-made and normalized technique (!)
which, in essence, can also be applied
to any other development process.
Despite the current 'simplicity' of this tutorial,
if anything is unclear about this tutorial
or if you have questions anyway,
just let me know!
You can contact me via my contact information page,
or simply by sending an email to TheChange@yahoo.com.
Have fun!
And thank you for reading!