Creating a Bullet effect (based on a Unity example)

Hello everyone, how are you? I hope you are all doing great.

I’ve recently been thinking about how to make a reusable bullet code with BabylonJS for the game I’m developing to publish on Steam:

I watched an exciting Unity tutorial and decided to implement something similar with BabylonJS.

Ultimately, I got a code very close to the Unity version, which works well (and it doesn’t need collision detection, which I think is good for performance). I want to share the code with you:

import Engine from "../core/Engine"
import type MainScene from "../scenes/MainScene"
import { Space, TransformNode, type InstancedMesh, type Vector3 } from "@babylonjs/core"

export class Bullet {
    protected scene: MainScene
    protected mesh: InstancedMesh
    protected transformNode: TransformNode

    protected target: Vector3 | null = null
    protected speed: number = 0.05
    
    protected onHit: (() => void) | null = null

    constructor(scene: MainScene, target: Vector3, position: Vector3) {
        this.scene = scene

        this.transformNode = new TransformNode("bulletTransform", this.scene.getScene())
        this.transformNode.position = position.clone()

        this.mesh = this.createMesh()

        this.seek(target)

        this.scene.getScene().onBeforeRenderObservable.add(() => {
            this.update()
        })
    }

    createMesh(): InstancedMesh {
        const mesh = this.scene.bulletMesh?.createInstance(`bullet`) as InstancedMesh

        mesh.setEnabled(true)
        mesh.parent = this.transformNode
        mesh.position.setAll(0)

        return mesh
    }

    seek(target: Vector3): void {
        this.target = target

        this.transformNode.lookAt(this.target)

        Engine.playAudio(this.getSound())
    }

    getSound(): string {
        return "gunSound"
    }

    setOnHit(callback: () => void): void {
        this.onHit = callback
    }

    update(): void {
        if(this.target === null) {
            this.destroy()
            return
        }

        const deltaTime = this.scene.getDeltaTime()

        const direction = this.target.subtract(this.transformNode.position)
        const distanceThisFrame = this.speed * deltaTime

        if(direction.length() < distanceThisFrame) {
            this.transformNode.position = this.target
            this.hitTarget()
            return
        }

        direction.normalize()
        this.transformNode.translate(direction, distanceThisFrame, Space.WORLD)
    }

    hitTarget(): void {
        if (this.onHit) {
            this.onHit()
        }

        this.destroy()
    }

    destroy(): void {
        this.target = null
        this.onHit = null
        
        this.mesh.dispose()
        this.transformNode.dispose()
    }
}

As you can see, it’s pretty simple to extend the class to create other types of shots:

import { Bullet } from "./Bullet"
import { type InstancedMesh } from "@babylonjs/core"

export class LaserShot extends Bullet {
    getSound(): string {
        return "laserShotSound"
    }

    createMesh(): InstancedMesh {
        const mesh = this.scene.laserShotMesh?.createInstance(`laserShot`) as InstancedMesh

        mesh.setEnabled(true)
        mesh.parent = this.transformNode
        mesh.position.setAll(0)

        return mesh
    }
}

And here is a simple example of its usage:

private async update(): Promise<void> {
    switch (this.state) {
       // ...

        case "ATTACKING":
            await this.attack()
            break
    } 
}

private async attack(): Promise<void> {
    // ...

    this.timeUntilFire += deltaTime
    if (this.timeUntilFire >= GameConfig.shipConfig.attackInterval) {
        this.timeUntilFire = 0
        await this.shoot()
    }
}

private async shoot(): Promise<void> {
    // ...

    const bullet = new Bullet(this.scene, this.target.getPosition(), this.model.position)
    bullet.setOnHit(() => {
        if (!this.target || !this.target.hasBuilding()) return
        this.target.building!.takeDamage(GameConfig.shipConfig.shootDamage)
    })

}

Hope this helps. Have a great weekend, everyone!

EDIT: added an example based on @labris base code:

2 Likes

Thanks for sharing.
A demo on the playground would be cool for see the result.

1 Like

I put the class into PG - https://playground.babylonjs.com/#WROYL5#2
But it is not easy to understand how it should work :slight_smile:

2 Likes

@Dad72 @labris Whoops, my bad. I should have added a Playground :blush:

@labris, Thanks for transferring the class to the Playground. I took the liberty of implementing the example using your code as a base :grin:

The current code version does not use collision detection, since it is not necessary for the game I am developing. Still, changing the logic to use collision detection instead of target distance is not too difficult.

Thanks and have a lovely week!

2 Likes