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: