Chain Lightning > 2D
Contents
Simple chain lightning effect
Demonstrates a simple method to create an effect of chain lightning, when one rectangle (enemy) is impacted by a bullet rectangle then a chain of lines is drawn between the centers of all enemies on screen. WASD keys move player, left mouse button to shoot and up arrow key to change zoom. The Player Bullets example can be used a starting point for this example. View on Github
Before you start
I am a self taught Go programmer and do it as a hobby, the code below is my own interpretation of how to do something, probably not the only way or the best way. This is intended as a resource to learn some basic Raylib and Go game dev skills. If you want to use any of the code anywhere else, feel free to do so.
Code
package main
import (
"math"
"math/rand"
rl "github.com/gen2brain/raylib-go/raylib"
)
/* MORE RAYLIB GO EXAMPLES ARE AVAILABLE HERE:
https://github.com/unklnik/raylib-go-more-examples
*/
var (
enemies []xenemy //SLICE OF ENEMY STRUCTS
bullets []xbullet //SLICE OF BULLET STRUCTS
shootTarget, cursorCam, cursor rl.Vector2 //TARGET, CURSOR CAMERA VS SCREEN POSITION, CURSOR
playerRec rl.Rectangle //PLAYER RECTANGLE
spd = float32(10) //MAX SPEED
attackT, chainT int32 //PAUSE BETWEEN BULLETS & LIGHTNING DRAW TIMER
fps = int32(60) //FRAMES PER SECOND
camera rl.Camera2D //CAMERA
borderRec rl.Rectangle //BOUNCING & BORDER RECTANGLES
cntr rl.Vector2 //CENTER OF SCREEN
scrW, scrH int //SCREEN WIDTH & HEIGHT
)
// STRUCT OF BULLET CONTAINING REC, DIRECTION X & Y, OFF BOOL
type xbullet struct {
rec rl.Rectangle
dirX, dirY float32
off bool
}
// STRUCT OF ENEMY CONTAINING REC, DIRECTION X & Y, HIT TIMER
type xenemy struct {
rec rl.Rectangle
dirX, dirY float32
hitT int32
}
func main() {
rl.InitWindow(0, 0, "chain lightning - raylib go - https://github.com/unklnik/raylib-go-more-examples")
scrW, scrH = rl.GetScreenWidth(), rl.GetScreenHeight() //GET SCREEN SIZES
rl.SetWindowSize(scrW, scrH) //SET WINDOW SIZE
rl.SetWindowState(rl.FlagBorderlessWindowedMode)
//rl.ToggleFullscreen() //UNCOMMENT IF YOU HAVE DISPLAY ISSUES WITH OVERLAPPING WINDOW BARS
rl.HideCursor() //HIDE THE STANDARD MOUSE CURSOR
cntr = rl.NewVector2(float32(scrW/2), float32(scrH/2)) //CALCULATE CENTER
borderRec = rl.NewRectangle(cntr.X-float32(scrW/4), cntr.Y-float32(scrH/4), float32(scrW/2), float32(scrH/2)) //DEFINE BORDER RECTANGLE
siz := float32(32) //PLAYER SIZE
playerRec = rl.NewRectangle(cntr.X-siz/2, cntr.Y-siz/2, siz, siz) //INITIAL PLAYER RECTANGLE
camera.Zoom = 1.5 //SETS CAMERA ZOOM
camera.Target = cntr //SET CAMERA TARGET
camera.Offset.X = float32(scrW / 2) //ADJUST CAMERA FOR ZOOM
camera.Offset.Y = float32(scrH / 2) //ADJUST CAMERA FOR ZOOM
makeEnemies()
rl.SetTargetFPS(fps) //NUMBER OF FRAMES DRAWN IN A SECOND
for !rl.WindowShouldClose() {
cursor = rl.GetMousePosition() //GET MOUSE POSITION
cursorCam = rl.GetScreenToWorld2D(cursor, camera) //GET MOUSE POSITION IN CAMERA SPACE WITH ZOOM
upEnemies() //UPDATE ENEMY MOVEMENTS
upBullets() //UPDATE BULLET MOVEMENTS & COLLISIONS
input() //CAPTURE INPUT
//TIMER
if attackT > 0 { //PAUSE BETWEEN SHOTS TIMER
attackT--
}
rl.BeginDrawing()
rl.ClearBackground(rl.Black)
rl.BeginMode2D(camera)
rl.DrawRectangleLinesEx(borderRec, 4, rl.Green) //DRAWS BORDER REC
rl.DrawRectangleLinesEx(playerRec, 8, rl.Magenta) //DRAW PLAYER REC
//DRAW ENEMIES
for i := 0; i < len(enemies); i++ {
if enemies[i].hitT > 0 {
rl.DrawRectangleLinesEx(enemies[i].rec, 2, rl.Magenta)
} else {
rl.DrawRectangleLinesEx(enemies[i].rec, 2, rl.Blue)
}
}
//DRAW CHAIN LIGHTING
if chainT > 0 {
chainT--
var cntrs []rl.Vector2 //STORE ALL THE RECTANGLE CENTERS IN A SLICE AS THEY MOVE
for i := 0; i < len(enemies); i++ {
cntrs = append(cntrs, rl.NewVector2(enemies[i].rec.X+enemies[i].rec.Width/2, enemies[i].rec.Y+enemies[i].rec.Height/2))
}
//DRAW LINES USING THESE CENTERS
for i := 0; i < len(cntrs); i++ {
if i < len(cntrs)-1 { //NOT ONE LESS THAN LENGTH
rl.DrawLineEx(cntrs[i], cntrs[i+1], 12, rl.Fade(rl.SkyBlue, 0.4))
}
}
}
//DRAW BULLETS
for i := 0; i < len(bullets); i++ {
if !bullets[i].off {
rl.DrawRectangleLinesEx(bullets[i].rec, 2, rl.Yellow)
}
}
//DRAW CIRCLE TARGET INSTEAD OF CURSOR
rl.DrawCircleLines(int32(cursorCam.X), int32(cursorCam.Y), 10, rl.Red)
rl.EndMode2D()
rl.DrawText("W A S D keys move", 10, 10, 20, rl.White)
rl.DrawText("left mouse to shoot", 10, 40, 20, rl.White)
rl.DrawText("up arrow key change zoom", 10, 70, 20, rl.White)
rl.EndDrawing()
}
rl.CloseWindow()
}
func makeEnemies() {
num := 4 //NUMBER OF ENEMIES TO MAKE
for num > 0 {
size := float32(32) //SIZE OF RECTANGLE
x := borderRec.X + size/2
y := borderRec.Y + size/2
x += rF32(0, borderRec.Width-size*2) //RANDOM POSITION WITHIN BORDER RECTANGLE
y += rF32(0, borderRec.Height-size*2)
zenemy := xenemy{}
zenemy.dirX = rF32(-spd, spd) //RANDOM SPEED SEE FUNCTION BELOW
zenemy.dirY = rF32(-spd, spd)
zenemy.rec = rl.NewRectangle(x, y, size, size)
enemies = append(enemies, zenemy)
num--
}
}
func upEnemies() {
for i := 0; i < len(enemies); i++ {
//UPDATE HIT TIMER
if enemies[i].hitT > 0 {
enemies[i].hitT--
}
//NEXT MOVEMENT RECTANGLE FOR RECTANGLE POINTS EXITING BORDER
checkRec := enemies[i].rec
checkRec.X += enemies[i].dirX
checkRec.Y += enemies[i].dirY
//VECTOR 2 POINTS OF FOUR CORNERS OF PLAYER RECTANGLE
v1 := rl.NewVector2(checkRec.X, checkRec.Y)
v2 := v1
v2.X += checkRec.Width
v3 := v2
v3.Y += checkRec.Height
v4 := v3
v4.X -= checkRec.Width
//CHECK IF VECTOR 2 HAS EXITED BORDER
canmove := true
if !rl.CheckCollisionPointRec(v1, borderRec) || !rl.CheckCollisionPointRec(v2, borderRec) || !rl.CheckCollisionPointRec(v3, borderRec) || !rl.CheckCollisionPointRec(v4, borderRec) {
canmove = false
}
if canmove {
enemies[i].rec = checkRec //MOVE TO NEW POSITION IF NO EXITS
} else {
//CHANGE DIRECTION IF NEXT VECTOR 2 HAS EXITED BORDER
enemies[i].dirX = rF32(-spd, spd)
enemies[i].dirY = rF32(-spd, spd)
}
}
}
func input() {
//INPUT KEYS FOR PLAYER MOVEMENT SEE MOVEPLAYER FUNCTION
if rl.IsKeyDown(rl.KeyW) {
movePlayer(1)
} else if rl.IsKeyDown(rl.KeyS) {
movePlayer(3)
}
if rl.IsKeyDown(rl.KeyD) {
movePlayer(2)
} else if rl.IsKeyDown(rl.KeyA) {
movePlayer(4)
}
//CREATE BULLET IF ATTACK TIMER IS ZERO
if rl.IsMouseButtonPressed(rl.MouseLeftButton) && attackT == 0 {
attackT = fps / 4
shootTarget = cursorCam //POSITION FOR BULLET AIMING
shoot()
}
//CHANGE ZOOM
if rl.IsKeyPressed(rl.KeyUp) {
if camera.Zoom == 2 {
camera.Zoom = 1
} else if camera.Zoom == 1.5 {
camera.Zoom = 2
} else if camera.Zoom == 1 {
camera.Zoom = 1.5
}
camera.Target = cntr
camera.Offset.X = float32(scrW / 2)
camera.Offset.Y = float32(scrH / 2)
}
}
// CREATE BULLET FUNCTION
func shoot() {
zbullet := xbullet{}
zbullet.rec = playerRec //DUPLICATE PLAYER RECTANGLE FOR BULLET
//MAKE DUPLICATE RECTANGLE SMALLER
zbullet.rec.X += playerRec.Width / 2
zbullet.rec.Y += playerRec.Height / 2
zbullet.rec.Width = zbullet.rec.Width / 2
zbullet.rec.Height = zbullet.rec.Height / 2
//CALCULATE X & Y SPEED TO MOVE TO SHOOT TARGET
playerCntr := rl.NewVector2(playerRec.X+playerRec.Width/2, playerRec.Y+playerRec.Height/2)
diffX := absdiff(playerCntr.X, shootTarget.X) //GET ABSOLUTE X DISTANCE FUNCTION BELOW
diffY := absdiff(playerCntr.Y, shootTarget.Y) //GET ABSOLUTE Y DISTANCE FUNCTION BELOW
if diffX > diffY {
zbullet.dirX = spd //IF DIFFERENCE X IS LARGER X IS FULL SPEED
zbullet.dirY = diffY / (diffX / zbullet.dirX) //CALCULATE Y SPEED
} else {
zbullet.dirY = spd //IF DIFFERENCE Y IS LARGER Y IS FULL SPEED
zbullet.dirX = diffX / (diffY / zbullet.dirY) //CALCULATE X SPEED
}
//IF TARGET IS BEHIND PLAYER CHANGE X DIRECTION TO NEGATIVE
if playerCntr.X > shootTarget.X {
zbullet.dirX = -zbullet.dirX
}
//IF TARGET IS ABOVE PLAYER CHANGE Y DIRECTION TO NEGATIVE
if playerCntr.Y > shootTarget.Y {
zbullet.dirY = -zbullet.dirY
}
//ADD BULLET TO SLICE
bullets = append(bullets, zbullet)
}
func upBullets() {
clear := false //TO CLEAR BULLETS IF COLLISIONS
for i := 0; i < len(bullets); i++ {
if !bullets[i].off {
for j := 0; j < len(enemies); j++ {
//CHECK FOR COLLISIONS VERSUS ENEMY RECTANGLES
if rl.CheckCollisionRecs(bullets[i].rec, enemies[j].rec) && enemies[j].hitT == 0 {
enemies[j].hitT = fps * 1 //TURN ON ENEMY HIT TIMER
bullets[i].off = true //TURN OFF BULLET
chainT = fps * 1 //TURN ON CHAIN LIGHTNING
}
}
}
//IF NO ENEMY COLLISIONS THEN CHECK BULLET MOVEMENT
if !bullets[i].off {
checkRec := bullets[i].rec //DUPLICATE RECTANGLE FOR NEXT COLLISIONS
checkRec.X += bullets[i].dirX //MOVE DUPLICATE TO NEXT POSITION
checkRec.Y += bullets[i].dirY
//VECTOR 2 POINTS OF FOUR CORNERS OF BULLET RECTANGLE
v1 := rl.NewVector2(checkRec.X, checkRec.Y)
v2 := v1
v2.X += bullets[i].rec.Width
v3 := v2
v3.Y += bullets[i].rec.Height
v4 := v3
v4.X -= bullets[i].rec.Width
//CHECK IF VECTOR 2 HAS EXITED BORDER
canmove := true
if !rl.CheckCollisionPointRec(v1, borderRec) || !rl.CheckCollisionPointRec(v2, borderRec) || !rl.CheckCollisionPointRec(v3, borderRec) || !rl.CheckCollisionPointRec(v4, borderRec) {
canmove = false
}
if canmove {
bullets[i].rec = checkRec //IF NO EXITS MOVE BULLET
} else {
bullets[i].off = true //IF EXITED TURN BULLET OFF
clear = true
}
}
}
//IF CLEAR IS ON REMOVE ALL OFF BULLETS FROM SLICE
if clear {
for i := 0; i < len(bullets); i++ {
if bullets[i].off {
bullets = remBullet(bullets, i)
}
}
}
}
func movePlayer(direc int) {
checkRec := playerRec //DUPLICATE PLAYER RECTANGLE
//MOVE DUPLICATE IN THE DIRECTION OF KEYPRESS
switch direc {
case 1: //UP
checkRec.Y -= spd
case 2: //RIGHT
checkRec.X += spd
case 3: //DOWN
checkRec.Y += spd
case 4: //LEFT
checkRec.X -= spd
}
//VECTOR 2 POINTS OF FOUR CORNERS OF PLAYER RECTANGLE
v1 := rl.NewVector2(checkRec.X, checkRec.Y)
v2 := v1
v2.X += playerRec.Width
v3 := v2
v3.Y += playerRec.Height
v4 := v3
v4.X -= playerRec.Width
//CHECK IF VECTOR 2 HAS EXITED BORDER
canmove := true
if !rl.CheckCollisionPointRec(v1, borderRec) || !rl.CheckCollisionPointRec(v2, borderRec) || !rl.CheckCollisionPointRec(v3, borderRec) || !rl.CheckCollisionPointRec(v4, borderRec) {
canmove = false
}
if canmove { //IF NO EXITS MOVE PLAYER
playerRec = checkRec
}
}
// REMOVES BULLET FROM SLICE
func remBullet(slice []xbullet, s int) []xbullet {
return append(slice[:s], slice[s+1:]...)
}
// GET ABSOLUTE DIFFERENCE
func absdiff(num1, num2 float32) float32 {
num := float32(0)
if num1 == num2 {
num = 0
} else {
if num1 <= 0 && num2 <= 0 {
num1 = getabs(num1)
num2 = getabs(num2)
if num1 > num2 {
num = num1 - num2
} else {
num = num2 - num1
}
} else if num1 <= 0 && num2 >= 0 {
num = num2 + getabs(num1)
} else if num2 <= 0 && num1 >= 0 {
num = num1 + getabs(num2)
} else if num2 >= 0 && num1 >= 0 {
if num1 > num2 {
num = num1 - num2
} else {
num = num2 - num1
}
}
}
return num
}
// GET ABSOLUTE VALUE
func getabs(value float32) float32 {
value2 := float64(value)
value = float32(math.Abs(value2))
return value
}
// RETURN RANDOM FLOAT32 BETWEEN TWO VALUES
func rF32(min, max float32) float32 {
min2 := float64(min)
max2 := float64(max)
return float32(min2 + rand.Float64()*(max2-min2))
}
Video
Want to give it a Go?
To start making games with Go and Raylib you will need:
- Go - https://go.dev/
- TDM-GCC - https://jmeubank.github.io/tdm-gcc/
- Git - https://git-scm.com/downloads
- Go Bindings for Raylib - https://github.com/gen2brain/raylib-go
- Visual Studio Code - https://code.visualstudio.com/
You can, of course, use other code editors however VS Code is my own personal preference