To run complex NPC behaviours I used web workers:
- Demon AI Decision
- Demon AI Vision
- Demon AI Pathfinding.
To run complex NPC behaviours I used web workers:
The last frame of your video is hilarious ![]()
The content is cool as well ![]()
WebWorkers are a relatively advanced feature that many developers are unfamiliar with. While SharedArrayBuffer already enables zero-copy shared memory between the main thread and worker threads, future evolutions of the WebWorkers API may further reduce or eliminate the need for explicit data transfer. I can’t wait for it!
And I love to put a face on a name! Nice to see you @onekit
Pre-New Year (final for 2025) progress report on the development of the game Gardener Shu.
#gardenexplorer #gardenershu #gamedev
Localization: Belarusian and Ukrainian languages added
Interface: Scrollbar added to the game menu
Upd. v0.11.7 gameplay
lol, I love the attitude: “I don’t want to harvest that crap - I wanna kick it, then shoot it!” ![]()
Gardener Shu — What’s new since 0.11.7 (now 0.19.0)
I didn’t post updates for a while, so here’s a condensed overview of what’s been added and improved.
Engine & tech
Babylon.js updated to 8.49.5;
Large refactors across the codebase; main-thread and worker memory leak fixes; optimized math (trig, random, constants).
New gameplay & content
New player model (toddler).
Wolves: full enemy with animations, hurt/death, sounds, wander, mushroom search, collision, 3D health bars, corpse handling; “wolf” cheat to spawn.
Helicopter: new enemy (spawn via “helicopter” cheat), ascent, banking movement, blade animation, hurt/death, mission destroy objectives.
Demons: triple jump attack, improved blood effects, diamond scaling by size, collision balance.
Jump & landing: three stages (soft landing, light damage, hard impact + stun), “fly” animation, higher run jump.
Shotgun: pellets in object pool, stronger impact and knockback.
Ammo: ammo packs (e.g. 25 rounds) with amount tracking; full vs partial pack models; reload/chamber sync with server.
Apple: healing scales with apple size (e.g. +6% / +15%).
Credits: dance during credits; Register (EULA); Google and Telegram login.
Camera & controls
Smooth third-person zoom with priority (user vs game); smooth camera roll reset after cinematics.
Scroll to switch camera: third-person ↔ POV ↔ Inventory; lower zoom sensitivity; mouse sensitivity up to 500% with proper save/restore.
Pointer lock and mouse sensitivity persist across level changes; camera sync fix when switching POV/third-person.
Multiplayer & network
Reliable handshake with retries and confirmation; level filtering so players only get updates for their level.
Inventory sync: pickUp, drop, reload, shoot, flashlight state; server connection monitor with on-screen status.
3D chat popovers above players; 3D health bars in world (billboard/vertical) instead of only 2D UI.
World & UI
3D selection box that follows terrain height; pants color from level data.
Fog fix on first load; settings merge so options (e.g. fog) load correctly.
Mission/tutorial text formatting; cheat codes “helicopter”, “wolf”, “iddqd”, “rich”; improved cheat storage.
Fixes & polish
(Network mode is under construction yet).
Early access: https://game.1kit.net
What’s new in version 0.21.9:
Asset packaging: implemented fflate for resource compression; now loading a single sounds.zip instead of making 200+ server requests for individual .ogg and .mp3 files.
Destructible rock: added a rock object (pre-fractured into 10 shards using the Blender Cell Fracture add-on).
New building: added a small house with round windows.
New particle systems: helicopter is on fire.
Wolf AI updates: the wolf is now more autonomous—it eats apples when health is low, hunts for mushrooms, and leads the player to their location.
Added a bug: level transitions are currently broken and player respawn is disabled. ![]()
This surfaced after switching from sequential to parallel asset loading. Since many game components are interdependent, the dependency chain is conflicting. I’m currently refactoring the code and aiming for a beta release in March 2026.
What’s new in version 0.25.6:
I had previously seen examples of similar shaders on the forum, but I always thought they would be too performance-heavy and costly to implement. After taking a closer look, I decided to give it a try and integrated it.
Demo: https://game.1kit.net
Click “Play” under “Single player mode.”
Once the game has loaded, click anywhere on the screen.
Press Enter and type “ocean” to switch to “shader ocean” mode.
Type “ocean” again to switch back to “WaterMaterial” mode.
how can you make the new ocean reflect sky
For now, I’m thinking of dynamically recoloring the water with the same color scheme, so that it darkens and lightens, and matches the sky’s tones. I don’t yet know how to achieve a mirror-like effect, like with WaterMaterial.
I can add more red color to the sunset time.
@withADoveInOneHand Here is an example of chameleon water: ![]()
It’s not a real reflection, but the sky and water can match colors.
I love the vibe.
What’s new in version 0.30.2:
WaterMaterial with Havok shape and particles system:
CreateDecal to create holes in the ground:
What’s new in 0.32.2:
fastSin and fastCos instead of the native Math.sin and Math.cos .![]()
Amazing !!! ![]()
What’s new in 0.41.8:
Obstacle transparency (dithering or x-ray)
When you move behind an object in isometric or third-person view, it becomes transparent so you don’t lose sight of the player and understand what’s happening to them. This will be much more convenient.
That’s such a great quality of life feature!
I’m using a shader that samples the ambient light intensity and adjusts the brightness, contrast, and other texture parameters of the mesh so it blends in with the game world. Since my game has a day/night cycle, I need that consistency. I also have an implementation using mesh.visibility , but I’ve been having trouble with it lately, so I decided to go the shader route instead.
DitherBehavior.ts
import { Environment } from '@game/environment'
import {
type AbstractMesh,
Color3,
Effect,
Engine,
type Material,
type Observer,
type Scene,
ShaderMaterial,
type Texture,
} from 'babylonjs'
const VERTEX_SHADER = `
precision highp float;
attribute vec3 position;
attribute vec2 uv;
attribute vec3 normal;
uniform mat4 world;
uniform mat4 worldViewProjection;
varying vec2 vUV;
varying vec3 vNormal;
void main() {
gl_Position = worldViewProjection * vec4(position, 1.0);
vUV = uv;
vNormal = normalize((world * vec4(normal, 0.0)).xyz);
}
`
const FRAGMENT_SHADER = `
precision highp float;
varying vec2 vUV;
varying vec3 vNormal;
uniform sampler2D diffuseSampler;
uniform float alpha;
uniform float hasDiffuseTexture;
uniform vec3 diffuseColor;
uniform float contrast;
uniform float brightness;
vec3 adjustContrast(vec3 color, float contrast) {
return (color - 0.5) * contrast + 0.5;
}
vec3 adjustBrightness(vec3 color, float brightness) {
return color + brightness;
}
void main() {
vec4 color = vec4(diffuseColor, 1.0);
if (hasDiffuseTexture > 0.5) {
color = texture2D(diffuseSampler, vUV);
}
color.rgb = adjustContrast(color.rgb, contrast);
color.rgb = adjustBrightness(color.rgb, brightness);
gl_FragColor = vec4(color.rgb, color.a * alpha);
}
`
let registered = false
function registerShader(): void {
if (registered) return
Effect.ShadersStore['ditherAlphaVertexShader'] = VERTEX_SHADER
Effect.ShadersStore['ditherAlphaFragmentShader'] = FRAGMENT_SHADER
registered = true
}
function createDitherMaterial(scene: Scene, hasTexture: boolean = false): ShaderMaterial {
registerShader()
const mat = new ShaderMaterial(
'ditherAlphaOcclusion',
scene,
{ vertex: 'ditherAlpha', fragment: 'ditherAlpha' },
{
attributes: ['position', 'uv', 'normal'],
uniforms: ['world', 'worldViewProjection', 'alpha', 'hasDiffuseTexture', 'diffuseColor', 'contrast', 'brightness'],
samplers: ['diffuseSampler'],
},
)
mat.setFloat('alpha', hasTexture ? 1.0 : 0.99)
mat.setFloat('hasDiffuseTexture', 0.0)
mat.setColor3('diffuseColor', new Color3(1, 1, 1))
mat.setFloat('contrast', 0.7)
mat.setFloat('brightness', 0.1)
mat.backFaceCulling = false
mat.alphaMode = Engine.ALPHA_COMBINE
if (hasTexture) {
mat.needAlphaBlending = () => true
}
return mat
}
function copyTexturesToShaderMaterial(original: Material, shader: ShaderMaterial): void {
// StandardMaterial - uses diffuseTexture
if ('diffuseTexture' in original) {
const diffuse = (original as { diffuseTexture: Texture | null }).diffuseTexture
if (diffuse) {
shader.setTexture('diffuseSampler', diffuse)
shader.setFloat('hasDiffuseTexture', 1.0)
return
}
// Also copy diffuse color
if ('diffuseColor' in original) {
const color = (original as { diffuseColor: { r: number; g: number; b: number } }).diffuseColor
shader.setColor3('diffuseColor', new Color3(color.r, color.g, color.b))
}
}
// PBRMaterial - uses albedoTexture
if ('albedoTexture' in original) {
const albedo = (original as { albedoTexture: Texture | null }).albedoTexture
if (albedo) {
shader.setTexture('diffuseSampler', albedo)
shader.setFloat('hasDiffuseTexture', 1.0)
return
}
// Also copy albedo color
if ('albedoColor' in original) {
const color = (original as { albedoColor: { r: number; g: number; b: number } }).albedoColor
shader.setColor3('diffuseColor', new Color3(color.r, color.g, color.b))
}
}
shader.setFloat('hasDiffuseTexture', 0.0)
shader.setColor3('diffuseColor', new Color3(1, 1, 1))
}
const TARGET_ALPHA = Number(import.meta.env.VITE_CAMERA_DITHER_ALPHA) || 0.3
const FADE_SPEED = Number(import.meta.env.VITE_CAMERA_DITHER_FADE_SPEED) || 0.1
enum DitherMode {
Shader = 'shader',
Visibility = 'visibility',
}
const DITHER_MODE = (import.meta.env.VITE_CAMERA_DITHER_MODE as DitherMode) || DitherMode.Shader
interface MaterialState {
original: Material | null
shader: ShaderMaterial
originalVisibility: number
originalAlpha?: number
originalTransparencyMode?: number
}
export default class DitherBehavior {
readonly name = 'ditherOcclusion'
attachedNode: AbstractMesh | null = null
private materialStates = new Map<string, MaterialState>()
private observer: Observer<Scene> | null = null
private cleaningUp = false
private fadingOut = true
private currentAlpha = 1.0
private targetAlpha = TARGET_ALPHA
init(): void {}
attach(target: AbstractMesh): void {
this.attachedNode = target
this.targetAlpha = TARGET_ALPHA
if (DITHER_MODE === DitherMode.Visibility) {
this.prepareVisibilityMode(target)
} else {
this.createShaderMaterials(target)
}
this.fadingOut = true
this.currentAlpha = 1.0
this.observer = target.getScene().onBeforeRenderObservable.add(() => {
this.tick()
})
}
private prepareVisibilityMode(target: AbstractMesh): void {
const states = new Map<string, MaterialState>()
const rootOriginal = target.material
if (rootOriginal) {
const originalVisibility = target.visibility
target.visibility = 1.0
const state = this.forceMaterialTransparency(rootOriginal)
states.set(target.uniqueId.toString(), {
original: rootOriginal,
shader: null as unknown as ShaderMaterial,
originalVisibility,
originalAlpha: state.originalAlpha,
originalTransparencyMode: state.originalTransparencyMode,
})
}
target.getChildMeshes().forEach((child) => {
const childOriginal = child.material
if (childOriginal) {
const originalVisibility = child.visibility
child.visibility = 1.0
const state = this.forceMaterialTransparency(childOriginal)
states.set(child.uniqueId.toString(), {
original: childOriginal,
shader: null as unknown as ShaderMaterial,
originalVisibility,
originalAlpha: state.originalAlpha,
originalTransparencyMode: state.originalTransparencyMode,
})
}
})
this.materialStates = states
}
private forceMaterialTransparency(_material: Material): { originalAlpha?: number; originalTransparencyMode?: number } {
const result = {
originalAlpha: undefined,
originalTransparencyMode: undefined,
}
return result
}
detach(): void {
if (this.cleaningUp) return
this.immediateRestore()
}
fadeOut(): void {
if (this.cleaningUp) return
this.fadingOut = true
this.targetAlpha = TARGET_ALPHA
}
dispose(): void {
if (this.cleaningUp) return
this.immediateRestore()
}
private immediateRestore(): void {
if (this.cleaningUp || !this.attachedNode) return
this.cleaningUp = true
const mesh = this.attachedNode
if (this.observer) {
const scene = mesh?.getScene()
if (scene) {
scene.onBeforeRenderObservable.remove(this.observer)
}
this.observer = null
}
if (mesh && !mesh.isDisposed()) {
if (DITHER_MODE === DitherMode.Visibility) {
this.restoreVisibility(mesh)
} else {
this.restoreMaterials(mesh)
}
mesh.removeBehavior(this)
}
this.materialStates.clear()
this.attachedNode = null
this.cleaningUp = false
}
private tick(): void {
if (this.cleaningUp || !this.attachedNode) return
const mesh = this.attachedNode
if (!mesh || mesh.isDisposed()) {
this.cleanup()
return
}
const diff = this.targetAlpha - this.currentAlpha
if (Math.abs(diff) < 0.005) {
this.currentAlpha = this.targetAlpha
if (!this.fadingOut && this.currentAlpha >= 0.995) {
this.cleanup()
}
return
}
this.currentAlpha += diff * FADE_SPEED
this.updateLightingUniforms()
this.applyAlphaToMesh(mesh)
}
private updateLightingUniforms(): void {
const light = Environment.light
if (!light) return
const lightColor = light.lightColor
const colorIntensity = (lightColor.r + lightColor.g + lightColor.b) / 3
const lightIntensity = light.lightIntensity ?? 1
const intensity = colorIntensity * lightIntensity
const baseContrast = 0.7
const baseBrightness = 0.0
const contrast = baseContrast * (1 - intensity * 0.05)
const brightness = baseBrightness + intensity * 0.05
this.materialStates.forEach((state) => {
if (state.shader) {
state.shader.setFloat('contrast', contrast)
state.shader.setFloat('brightness', brightness)
}
})
}
private applyAlphaToMesh(mesh: AbstractMesh): void {
if (DITHER_MODE === DitherMode.Visibility) {
this.applyVisibilityToMesh(mesh)
} else {
this.applyShaderToMesh(mesh)
}
}
private applyShaderToMesh(mesh: AbstractMesh): void {
const rootState = this.materialStates.get(mesh.uniqueId.toString())
if (rootState?.shader) {
rootState.shader.setFloat('alpha', this.currentAlpha)
}
mesh.getChildMeshes().forEach((child) => {
if (!child.isDisposed()) {
const childState = this.materialStates.get(child.uniqueId.toString())
if (childState?.shader) {
childState.shader.setFloat('alpha', this.currentAlpha)
}
}
})
}
private applyVisibilityToMesh(mesh: AbstractMesh): void {
const rootState = this.materialStates.get(mesh.uniqueId.toString())
if (rootState) {
const targetVisibility = rootState.originalVisibility * this.currentAlpha
mesh.visibility = Math.max(0.0, Math.min(1.0, targetVisibility))
}
mesh.getChildMeshes().forEach((child) => {
if (!child.isDisposed()) {
const childState = this.materialStates.get(child.uniqueId.toString())
if (childState) {
const targetVisibility = childState.originalVisibility * this.currentAlpha
child.visibility = Math.max(0.0, Math.min(1.0, targetVisibility))
}
}
})
}
private cleanup(): void {
if (this.cleaningUp) return
this.cleaningUp = true
const mesh = this.attachedNode
if (this.observer) {
const scene = mesh?.getScene()
if (scene) {
scene.onBeforeRenderObservable.remove(this.observer)
}
this.observer = null
}
if (mesh && !mesh.isDisposed()) {
if (DITHER_MODE === DitherMode.Visibility) {
this.restoreVisibility(mesh)
} else {
this.restoreMaterials(mesh)
}
mesh.removeBehavior(this)
}
this.materialStates.clear()
this.attachedNode = null
this.cleaningUp = false
}
private restoreVisibility(mesh: AbstractMesh): void {
const rootState = this.materialStates.get(mesh.uniqueId.toString())
if (rootState) {
mesh.visibility = rootState.originalVisibility
this.restoreMaterialTransparency(rootState.original, rootState.originalAlpha, rootState.originalTransparencyMode)
}
mesh.getChildMeshes().forEach((child) => {
if (!child.isDisposed()) {
const childState = this.materialStates.get(child.uniqueId.toString())
if (childState) {
child.visibility = childState.originalVisibility
this.restoreMaterialTransparency(childState.original, childState.originalAlpha, childState.originalTransparencyMode)
}
}
})
}
private restoreMaterialTransparency(material: Material | null, originalAlpha?: number, originalTransparencyMode?: number): void {
if (!material) return
const pbrMat = material as unknown as {
transparencyMode?: number
alpha?: number
}
if (originalTransparencyMode !== undefined) {
pbrMat.transparencyMode = originalTransparencyMode
}
if (originalAlpha !== undefined) {
pbrMat.alpha = originalAlpha
}
}
private restoreMaterials(mesh: AbstractMesh): void {
mesh.material = this.materialStates.get(mesh.uniqueId.toString())?.original ?? null
mesh.getChildMeshes().forEach((child) => {
if (!child.isDisposed()) {
child.material = this.materialStates.get(child.uniqueId.toString())?.original ?? null
}
})
for (const state of this.materialStates.values()) {
if (state.shader) {
state.shader.dispose()
}
}
}
private createShaderMaterials(target: AbstractMesh): void {
const scene = target.getScene()
const states = new Map<string, MaterialState>()
const originalVisibility = target.visibility
target.visibility = 1.0
const rootOriginal = target.material
if (rootOriginal) {
const hasTexture = this.checkHasTexture(rootOriginal)
const shader = createDitherMaterial(scene, hasTexture)
copyTexturesToShaderMaterial(rootOriginal, shader)
shader.setFloat('alpha', this.currentAlpha)
target.material = shader
states.set(target.uniqueId.toString(), { original: rootOriginal, shader, originalVisibility })
}
target.getChildMeshes().forEach((child) => {
const childOriginal = child.material
if (childOriginal) {
const childOriginalVisibility = child.visibility
child.visibility = 1.0
const hasTexture = this.checkHasTexture(childOriginal)
const shader = createDitherMaterial(scene, hasTexture)
copyTexturesToShaderMaterial(childOriginal, shader)
shader.setFloat('alpha', this.currentAlpha)
child.material = shader
states.set(child.uniqueId.toString(), { original: childOriginal, shader, originalVisibility: childOriginalVisibility })
}
})
this.materialStates = states
}
private checkHasTexture(material: Material): boolean {
if ('diffuseTexture' in material && material.diffuseTexture) return true
if ('albedoTexture' in material && material.albedoTexture) return true
return false
}
}
AlphaFadeShader.ts
import { Effect, type Scene, ShaderMaterial } from 'babylonjs'
const VERTEX_SHADER = `
precision highp float;
attribute vec3 position;
attribute vec2 uv;
attribute vec3 normal;
uniform mat4 world;
uniform mat4 worldViewProjection;
varying vec2 vUV;
varying vec3 vNormal;
varying vec3 vWorldPos;
void main() {
gl_Position = worldViewProjection * vec4(position, 1.0);
vUV = uv;
vNormal = normalize((world * vec4(normal, 0.0)).xyz);
vWorldPos = (world * vec4(position, 1.0)).xyz;
}
`
const FRAGMENT_SHADER = `
precision highp float;
varying vec2 vUV;
varying vec3 vNormal;
varying vec3 vWorldPos;
uniform sampler2D diffuseSampler;
uniform float alpha;
uniform float hasDiffuseTexture;
uniform float contrast;
uniform float brightness;
vec3 adjustContrast(vec3 color, float contrast) {
return (color - 0.5) * contrast + 0.5;
}
vec3 adjustBrightness(vec3 color, float brightness) {
return color + brightness;
}
void main() {
vec4 color = vec4(1.0, 1.0, 1.0, 1.0);
if (hasDiffuseTexture > 0.5) {
color = texture2D(diffuseSampler, vUV);
}
color.rgb = adjustContrast(color.rgb, contrast);
color.rgb = adjustBrightness(color.rgb, brightness);
gl_FragColor = vec4(color.rgb, color.a * alpha);
}
`
let registered = false
function registerAlphaFadeShader(): void {
if (registered) return
Effect.ShadersStore['alphaFadeVertexShader'] = VERTEX_SHADER
Effect.ShadersStore['alphaFadeFragmentShader'] = FRAGMENT_SHADER
registered = true
}
const TARGET_ALPHA = Number(import.meta.env.VITE_CAMERA_DITHER_ALPHA) || 0.3
export function createAlphaFadeMaterial(scene: Scene): ShaderMaterial {
registerAlphaFadeShader()
const mat = new ShaderMaterial(
'alphaFadeOcclusion',
scene,
{ vertex: 'alphaFade', fragment: 'alphaFade' },
{
attributes: ['position', 'uv', 'normal'],
uniforms: ['world', 'worldViewProjection', 'alpha', 'hasDiffuseTexture', 'contrast', 'brightness'],
samplers: ['diffuseSampler'],
},
)
mat.setFloat('alpha', TARGET_ALPHA)
mat.setFloat('hasDiffuseTexture', 0.0)
mat.setFloat('contrast', 0.8)
mat.setFloat('brightness', 0.02)
mat.backFaceCulling = true
mat.needAlphaBlending = () => true
return mat
}
Amazing updates ![]()
@onekit I had a question about your video:
Around 23 seconds, I see that the weapon is sort of jittering away from the player. I think I saw this exact bug in my game too (as the character moved away from the origin (0, 0, 0)). The source of the error for me was that the weapon’s position was being set as Float16s instead of Float32s. I’m wondering if this is the same in your case.
If not, maybe Babylon 9.0’s Large World Rendering could be helpful here?
Thanks for pointing out that issue.
I need to switch the implementation from manual weapon positioning to attachToBone .
I’ll fix it so the weapon no longer trails behind the player but is firmly attached to the hand bone.