← Public packages
@kentcdodds/pirate-shooter
Vertical scrolling 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 = 34
const SEA_HEIGHT = 52
const SEA_CENTER_Y = 6
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, 34, 52)
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]!
const y = this.baseY[i]! + this.scroll
const swell = Math.sin(x * 0.6 + this.time * 1.3) * Math.cos(y * 0.45 + this.time * 0.9)
const chop = Math.sin(y * 0.9 + x * 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 minY = SEA_CENTER_Y - SEA_HEIGHT / 2
for (let i = 0; i < FOAM_COUNT; i++) {
let y = foamPositions.getY(i) - SCROLL_SPEED * dt
if (y < minY) {
y += SEA_HEIGHT
foamPositions.setX(i, THREE.MathUtils.randFloatSpread(SEA_WIDTH))
}
foamPositions.setY(i, y)
}
foamPositions.needsUpdate = true
}
}