Chain Lightning > 2D

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.

go



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))
}


Want to give it a Go?

To start making games with Go and Raylib you will need:

You can, of course, use other code editors however VS Code is my own personal preference

Related Content