← Public packages
@kentcdodds/pirate-side-scroller
Left-to-right pirate ship shooter on the open sea, built with Three.js for phone or desktop
src/sea.ts
90 lines · 2.9 KB · TypeScriptimport * as THREE from 'three'
const SURFACE_Z = -0.2
const WAVE_HEIGHT = 0.22
const SCROLL_SPEED = 5
const FOAM_COUNT = 120
// Covers everything the tilted camera can see, which reaches well past the play field.
const SEA_WIDTH = 50
const SEA_HEIGHT = 28
const SEA_CENTER_Y = 2.5
export class Sea {
readonly object = new THREE.Group()
private readonly water: THREE.Mesh<THREE.PlaneGeometry, THREE.MeshStandardMaterial>
private readonly foam: THREE.Points<THREE.BufferGeometry, THREE.PointsMaterial>
private readonly baseX: Float32Array
private readonly baseY: Float32Array
private time = 0
private scroll = 0
constructor() {
const geometry = new THREE.PlaneGeometry(SEA_WIDTH, SEA_HEIGHT, 50, 28)
geometry.translate(0, SEA_CENTER_Y, 0)
const positions = geometry.attributes.position!
this.baseX = new Float32Array(positions.count)
this.baseY = new Float32Array(positions.count)
for (let i = 0; i < positions.count; i++) {
this.baseX[i] = positions.getX(i)
this.baseY[i] = positions.getY(i)
}
this.water = new THREE.Mesh(
geometry,
new THREE.MeshStandardMaterial({
color: 0x0e6aa8,
flatShading: true,
roughness: 0.6,
}),
)
this.water.position.z = SURFACE_Z
this.object.add(this.water)
const foamPositions = new Float32Array(FOAM_COUNT * 3)
for (let i = 0; i < FOAM_COUNT; i++) {
foamPositions[i * 3] = THREE.MathUtils.randFloatSpread(SEA_WIDTH)
foamPositions[i * 3 + 1] = SEA_CENTER_Y + THREE.MathUtils.randFloatSpread(SEA_HEIGHT)
foamPositions[i * 3 + 2] = SURFACE_Z + WAVE_HEIGHT + 0.05
}
const foamGeometry = new THREE.BufferGeometry()
foamGeometry.setAttribute('position', new THREE.BufferAttribute(foamPositions, 3))
this.foam = new THREE.Points(
foamGeometry,
new THREE.PointsMaterial({
color: 0xe0f2fe,
size: 0.25,
transparent: true,
opacity: 0.5,
depthWrite: false,
}),
)
this.object.add(this.foam)
}
update(dt: number) {
this.time += dt
this.scroll += dt * SCROLL_SPEED
const positions = this.water.geometry.attributes.position!
for (let i = 0; i < positions.count; i++) {
const x = this.baseX[i]! + this.scroll
const y = this.baseY[i]!
const swell = Math.sin(y * 0.6 + this.time * 1.3) * Math.cos(x * 0.45 + this.time * 0.9)
const chop = Math.sin(x * 0.9 + y * 0.3 + this.time * 2) * 0.4
positions.setZ(i, (swell + chop) * WAVE_HEIGHT)
}
positions.needsUpdate = true
const foamPositions = this.foam.geometry.attributes.position!
const minX = -SEA_WIDTH / 2
for (let i = 0; i < FOAM_COUNT; i++) {
let x = foamPositions.getX(i) - SCROLL_SPEED * dt
if (x < minX) {
x += SEA_WIDTH
foamPositions.setY(i, SEA_CENTER_Y + THREE.MathUtils.randFloatSpread(SEA_HEIGHT))
}
foamPositions.setX(i, x)
}
foamPositions.needsUpdate = true
}
}