SWAG

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 :

External dependencies

Dependencies are other modules you depend on. For example for Flappy, we will use the gui module.

Additional files

In case of scripts, you can add more files to compile with the #load directive.

Module configuration

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