Here is a very simple script that implements the Flappy Bird game. To have some fun and play with it, go to the bin/examples/scripts folder, and type :
$ swag flappy.swgs
The goal here is to comment a real-life example of Swag usage. However, it's better to read the basic language documentation first, as we will not go too deep into the details.
Note
This page has been generated with Swag directly from the source code.
So, let's begin.
Dependencies
Typically, you'd place the #dependency block inside the module.swg file of a module. However, if you're working with a script and there isn't a module.swg file, simply put it at the beginning of the script file.
This special #dependencies compiler block is used to specify :
Dependencies are other modules you depend on. For example for Flappy, we will use the gui module.
In case of scripts, you can add more files to compile with the #load directive.
If present, a special #run block will be executed by the compiler at the very beginning of the compilation stage. It gives the opportunity to change some build configurations.
So in our case, we need to import the module gui. This module is used to create windows and widgets, and will bring other modules like core and pixel (2D painting).
#dependencies
{
// The location "swag@std" tells swag that 'gui' is a standard module that is located
// with the compiler.
#import "gui" location="swag@std"
// Import the audio module, from the same default location
#import "audio" location="swag@std"
// This is the optional '#run' block executed by the compiler before compiling the script itself.
// We define it just to make the point because Flappy does not really need one.
#run
{
// Get the compiler interface to communicate with the compiler.
let itf = @compiler()
// Get the build configuration
let cfg = itf.getBuildCfg()
// Change something...
// Here, for example, we force all safety guards to be present in 'debug', and we remove all
// of them in 'release'.
#if #cfg == "debug":
cfg.safetyGuards = Swag.SafetyAll
#else:
cfg.safetyGuards = Swag.SafetyNone
}
}
Note
You might observe that there's no need to end the lines with a ; like in C or C++. While it's still possible, this is not mandatory.
Every module has its individual namespace. To avoid the necessity of mentioning it each time we wish to reference something, we include a global using statement immediately after the #dependency block.
The gui module depends on pixel which depends on core. So we bring all the three namespaces into the file scope. Note that we keep Audio as it is.
using Core, Pixel, Gui
Entry point
The #main special function is usually the entry point of an executable. It's equivalent to the well known main() function, but without arguments. In the case of a script, this special function will be executed by the compiler as the program entry point.
Note
You might observe that the arrangement of global declarations doesn't make a difference, as we're using the onEvent function before even defining it. Swag does not bother about the global declaration order.
#main
{
// Creates audio engine. 'assume' tells Swag that if the creation fails, we should panic.
assume Audio.createEngine()
// Thanks to 'defer', the audio engine will be destroyed when
// leaving this scope, i.e. when exiting the '#main' function.
defer Audio.destroyEngine()
// From the command line, if the script is run with '--arg:swag.test', then we force the application
// to exit after 100 frames. This is usefull for batch testing.
func test(app: *Application) = if Env.hasArg("swag.test"):
app.maxRunFrame = 100
// Creates and run one surface (i.e. window) at the given position and with the given size and title.
// 'hook' defines a lambda that will receive and treat all gui events
// 'init' defines a lambda that will be called for surface initialization
Application.runSurface(100, 100, 300, 512, title: "Flappy Bird", hook: &onEvent, init: &test)
}
Global definitions
Constants
We declare global constants with const. Note that we: not specify types for thoses constants. They will be deduced thanks to the affection.
const Gravity = 2.5 // 2.5 is a 32 bits float, so the type of Gravity is 'f32'
const GroundHeight = 40.0
const SpeedHorz = 100.0
const BirdImpulseY = 350 // 350 is an integer, so the type of BirdImpulseY is 's32'
Variables
var g_Bird: Bird // 'Bird' is a structure, and will be defined later
g_Pipes is a dynamic and generic array where all the elements are of type Pipe. In other languages, you would write Array<Pipe>. Array comes from the Core module, but thanks to the global using Core, we: not need to write Core.Array'Pipe.
var g_Pipes: Array'Pipe
Math is a namespace part of the Core module. We could have specified a global using Core.Math at the top of the script file, but here, we prefer the explicit reference.
var g_Rect: Math.Rectangle
Here are a bunch of global variables. All of them are initialized to 0 by default.
var g_Dt: f32
var g_Time: f32
var g_BasePos: f32
var g_Score: s32
var g_GameOver: bool
var g_Start: bool
Again, no need to specify the type of g_FirstStart. It is deduced from the affectation to be bool.
var g_FirstStart = true
// Texture assets
var g_DigitTexture: [10] Texture // A static array of 10 textures
var g_BirdTexture: [3] Texture // A static array of 3 textures
var g_BackTexture: Texture
var g_OverTexture: Texture
var g_BaseTexture: Texture
var g_PipeTextureU: Texture
var g_PipeTextureD: Texture
var g_MsgTexture: Texture
// Sound assets
var g_SoundDie: Audio.SoundFile
var g_SoundScore: Audio.SoundFile
var g_SoundHit: Audio.SoundFile
var g_SoundFlap: Audio.SoundFile
g_Font is a value pointer to a Font structure.
var g_Font: *Font
In Swag, their are two types of pointers.
- value pointers, which points to one single value. Declared with *
- block pointers, which points to multiple values. Declared with ^.
Pointer arithmetic is not enabled on value pointers, but it is on block pointers.
Types
// Defines the Bird
struct Bird
{
// Position of the bird. Full qualified name.
pos: Core.Math.Vector2
// Speed of the bird. In fact no need to specify 'Core' thanks to the global 'using'.
speed: Math.Vector2
// Sprite frame
frame: f32
}
// Defines one Pipe
struct Pipe
{
rectUp: Math.Rectangle // Position of the up part of the Pipe
rectDown: Math.Rectangle // Position of the down part of the Pipe
distToNext: f32 // Distance to the next Pipe
scored: bool // 'true' if the Bird has passed that Pipe
}
The actual code
This is the callback that will deal with all gui events. This feels like Windows API, but there are other ways of dealing with gui, in a more 'object like' way. You can look at the captme tool for example, which does not use a callback but interfaces instead.
For a simple script, this is more easy to process events in that way.
func onEvent(wnd: *Wnd, evt: *Event)->bool
{
switch evt.kind
{
case Create:
g_Rect = wnd.getClientRect()
// 'loadAssets' can raise some errors (we'll see later). So here we 'assume' that
// everything will be fine. If an error is raised, a 'panic' will occur.
assume loadAssets(wnd)
start()
case Resize:
g_Rect = wnd.getClientRect()
case Paint:
let paintEvt = cast(*PaintEvent) evt
let painter = paintEvt.bc.painter
// This is the elapsed time between two 'frames', in seconds.
g_Dt = wnd.getApp().getDt()
input(wnd) // IO (keyboard test)
paint(painter) // Paint game
move() // Update game
// We force the window to be dirty, which means that the 'Paint' event will
// be sent again next frame.
// That way we have a cheap 'frame' update.
wnd.invalidate()
}
return false
}
This is the function to paint the game. Here we are mostly using functions from the Pixel module.
func paint(painter: *Painter)
{
// Draw the background texture at position (0, 0). The size of the texture is preserved.
painter.drawTexture(0, 0, g_BackTexture)
// Bird
if !g_FirstStart
{
// We save the state of the painter, in order to restore it at the end of the scope.
// The state contains various painting parameters, like the current transformation.
painter.pushState()
defer painter.popState()
var trf = painter.getTransform()
painter.resetTransform()
let speed = Math.clamp(g_Bird.speed.y, -200, 200)
let angle = Math.map(speed, -200, 200, -Math.ConstF32.PiBy6, Math.ConstF32.PiBy6)
painter.rotateTransform(angle)
painter.translateTransform(g_Bird.pos.x + trf.tx, g_Bird.pos.y + trf.ty)
// This is the sprite frame of the bird
let frame = cast(s32) g_Bird.frame % 3
// No need to initialize the 'pt' variable here, as we will set both 'x' and 'y'
// just after. So we initialize it with 'undefined'.
var pt: Math.Point = undefined
pt.x = -g_BirdTexture[frame].width * 0.5
pt.y = -g_BirdTexture[frame].height * 0.5
// Add a little position wave
if !g_Start
{
pt.y += 5 * Math.sin(g_Time)
g_Time += 5 * g_Dt
}
painter.drawTexture(pt.x, pt.y, g_BirdTexture[frame])
}
// Draw pipes.
// 'foreach' for on all the pipes stored in the dynamic array, and returns each value
// as a pointer/reference.
foreach pipe in g_Pipes
{
painter.drawTexture(pipe.rectUp, g_PipeTextureU)
painter.drawTexture(pipe.rectDown, g_PipeTextureD)
}
// Base
painter.drawTexture(-g_BasePos, g_Rect.bottom() - GroundHeight, cast(f32) g_BaseTexture.width, GroundHeight, g_BaseTexture)
painter.drawTexture(-g_BasePos + g_BaseTexture.width, g_Rect.bottom() - GroundHeight, cast(f32) g_BaseTexture.width, GroundHeight, g_BaseTexture)
if !g_GameOver:
g_BasePos += SpeedHorz * g_Dt
if g_BasePos >= g_BaseTexture.width:
g_BasePos = 0
// Gameover text, centered
if g_GameOver
{
// This is another way of initializing a struct variable or constant.
var pt = Math.Point{g_Rect.horzCenter() - g_OverTexture.width / 2, g_Rect.vertCenter() - g_OverTexture.height / 2}
painter.drawTexture(pt.x, pt.y, g_OverTexture)
}
// Score
if !g_FirstStart
{
painter.drawStringCenter(g_Rect.horzCenter(), 50, Format.toString("%", g_Score), g_Font, Argb.White)
}
// Message
if g_FirstStart
{
var pt: Math.Point = undefined
pt.x = g_Rect.horzCenter() - g_MsgTexture.width / 2
pt.y = g_Rect.vertCenter() - g_MsgTexture.height / 2
painter.drawTexture(pt.x, pt.y, g_MsgTexture)
}
}
Test IO.
func input(wnd: *Wnd)
{
let kb = wnd.getApp().getKeyboard()
if g_GameOver or g_FirstStart
{
if kb.isKeyJustPressed(.Space)
{
start()
g_FirstStart = false
}
return
}
if kb.isKeyJustPressed(.Up)
{
g_Start = true
g_Bird.speed.y = -BirdImpulseY
assume Audio.Voice.play(&g_SoundFlap)
}
}
Collision detection (kind of) between the bird and a given rectangle.
func birdInRect(rect: Math.Rectangle)->bool
{
var rectBird: Math.Rectangle = undefined
rectBird.x = g_Bird.pos.x - g_BirdTexture[0].width / 2
rectBird.y = g_Bird.pos.y - g_BirdTexture[0].height / 2
rectBird.width = g_BirdTexture[0].width
rectBird.height = g_BirdTexture[0].height
return rect.intersectWith(rectBird)
}
The update part of the game.
func move()
{
if g_GameOver:
return
g_Bird.pos += g_Bird.speed * g_Dt
g_Bird.pos.y = Math.max(g_Bird.pos.y, 0)
if g_Start:
g_Bird.speed += {0, Gravity}
g_Bird.frame += 10 * g_Dt
// Be sure to have at least one pipe
if g_Pipes.count == 0:
createPipe()
// Move each pipe, and test collisions against the bird
if g_Start
{
foreach &pipe in g_Pipes
{
pipe.rectUp.x -= SpeedHorz * g_Dt
pipe.rectDown.x -= SpeedHorz * g_Dt
// Collision with the pipes
if birdInRect(pipe.rectUp) or birdInRect(pipe.rectDown)
{
assume Audio.Voice.play(&g_SoundHit)
g_GameOver = true
}
// If bird moves to the right of a pipe, then this is a win !
// Score up.
if g_Bird.pos.x > pipe.rectUp.right() and !pipe.scored
{
assume Audio.Voice.play(&g_SoundScore)
pipe.scored = true
g_Score += 1
}
}
}
// If the first pipe is out of screen, remove it
if g_Pipes[0].rectUp.right() < 0:
g_Pipes.removeAt(0)
// If the last pipe is enough inside, create a new one
if g_Rect.width - g_Pipes.back().rectUp.right() > g_Pipes.back().distToNext:
createPipe()
// Collision with the ground
if g_Bird.pos.y + g_BirdTexture[0].height > g_Rect.bottom() - GroundHeight
{
g_GameOver = true
assume Audio.Voice.play(&g_SoundHit)
}
// Play dying sound
if g_GameOver:
assume Audio.Voice.play(&g_SoundDie)
}
Creates a random up and down part of a new Pipe.
func createPipe()
{
let pos = g_Rect.width
let sizePassage = Random.shared().nextF32(100, 175)
let heightUp = Random.shared().nextF32(50, g_Rect.height - sizePassage - 50)
let heightDown = g_Rect.height - heightUp - GroundHeight - sizePassage
var pipe: Pipe
pipe.rectUp.x = pos
pipe.rectUp.y = heightUp - g_PipeTextureU.height
pipe.rectUp.width = g_PipeTextureU.width
pipe.rectUp.height = g_PipeTextureU.height
pipe.rectDown.x = pos
pipe.rectDown.y = g_Rect.bottom() - heightDown - GroundHeight
pipe.rectDown.width = g_PipeTextureU.width
pipe.rectDown.height = g_PipeTextureU.height
pipe.distToNext = Random.shared().nextF32(100, 200)
g_Pipes.add(pipe)
}
Reinitialize the game.
func start()
{
// '@init' can be called to reinitialize a variable to its default value
@init(&g_Bird, 1)
g_Bird.pos.x = g_Rect.horzCenter()
g_Bird.pos.y = g_Rect.vertCenter()
g_Score = 0
g_Pipes.clear()
// Assign two variables with the same value
g_GameOver, g_Start = false
}
Load all assets.
You can notice that the function has throw after its declaration. This indicated that it can return some errors, and that the caller will have to deal with it.
func loadAssets(wnd: *Wnd) throw
{
let render = &wnd.getApp().renderer
var dataPath: String = Path.getDirectoryName(#curlocation.fileName)
dataPath = Path.combine(dataPath, "datas")
dataPath = Path.combine(dataPath, "flappy")
// Load all sounds
//
// 'with' tells the compiler that what follows is in the 'context' of the namespace 'Audio.SoundFile'.
// So instead of having to write 'Audio.SoundFile.load(...)', we can now only write '.load(...)'.
with Audio.SoundFile
{
g_SoundDie = .load(Path.combine(dataPath, "die.wav"))
g_SoundScore = .load(Path.combine(dataPath, "point.wav"))
g_SoundHit = .load(Path.combine(dataPath, "hit.wav"))
g_SoundFlap = .load(Path.combine(dataPath, "flap.wav"))
}
// Load all textures
//
// 'with' can also be used with a variable.
with render
{
g_BirdTexture[0] = .addImage(Path.combine(dataPath, "yellowbird-upflap.png"))
g_BirdTexture[1] = .addImage(Path.combine(dataPath, "yellowbird-midflap.png"))
g_BirdTexture[2] = .addImage(Path.combine(dataPath, "yellowbird-downflap.png"))
g_OverTexture = .addImage(Path.combine(dataPath, "gameover.png"))
g_BaseTexture = .addImage(Path.combine(dataPath, "base.png"))
g_BackTexture = .addImage(Path.combine(dataPath, "background-day.png"))
g_MsgTexture = .addImage(Path.combine(dataPath, "message.png"))
}
var img = Image.load(Path.combine(dataPath, "pipe-green.png"))
g_PipeTextureD = render.addImage(img)
img.flip()
g_PipeTextureU = render.addImage(img)
g_Font = Font.create(Path.combine(dataPath, "FlappyBirdy.ttf"), 50)
}
Generated on 08-09-2024 with
swag 0.40.0