MOODUL 11 · TASE 4 — EDASIJÕUDNUD+

🏗️ 3D Mängumootor Ehitamine

Füüsikamootor Cannon.js + Rapier, 3D kokkupõrked, ECS 3D-s, scene graph — ehita oma 3D mängumootori alus!

⏱️ ~8-10 tundi 📝 7 harjutust 🔴 Edasijõudnud+

⚡ Samm 1 — Füüsikamootor (Cannon-es)

Terminal
npm install cannon-es  # Cannon.js TypeScript fork
physics.ts — Füüsikamaailm
import * as CANNON from 'cannon-es';
import * as THREE from 'three';

// Füüsikamaailm
const world = new CANNON.World({
  gravity: new CANNON.Vec3(0, -9.82, 0),
});
world.broadphase = new CANNON.SAPBroadphase(world);

// Põrand (staatiline keha)
const floorBody = new CANNON.Body({
  type: CANNON.Body.STATIC,
  shape: new CANNON.Plane(),
});
floorBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(floorBody);

// Dünaamiline kuubik
const boxBody = new CANNON.Body({
  mass: 1,
  shape: new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)),
  position: new CANNON.Vec3(0, 5, 0),
});
world.addBody(boxBody);

// Three.js mesh
const boxMesh = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshStandardMaterial({ color: 0x22c55e })
);
scene.add(boxMesh);

// Sünkroniseeri füüsika ja graafika
function updatePhysics(delta: number): void {
  world.step(1 / 60, delta, 3);

  // Kopeeri füüsika positsioon mesh'ile
  boxMesh.position.copy(boxBody.position as any);
  boxMesh.quaternion.copy(boxBody.quaternion as any);
}

// Jõu rakendamine
function applyForce(direction: CANNON.Vec3): void {
  boxBody.applyForce(direction, boxBody.position);
}

// Impulss (ühekordne jõud, nt hüpe)
function jump(): void {
  if (isGrounded()) {
    boxBody.applyImpulse(new CANNON.Vec3(0, 8, 0), boxBody.position);
  }
}

function isGrounded(): boolean {
  // Ray cast alla
  const ray = new CANNON.Ray(boxBody.position, new CANNON.Vec3(0, -1, 0));
  const result = new CANNON.RaycastResult();
  ray.intersectWorld(world, { result, skipBackfaces: true });
  return result.hasHit && result.distance < 0.6;
}

🧱 Samm 2 — Physics Body Manager

PhysicsManager.ts
interface PhysicsObject {
  body: CANNON.Body;
  mesh: THREE.Mesh;
}

class PhysicsManager {
  private world: CANNON.World;
  private objects: PhysicsObject[] = [];

  constructor() {
    this.world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) });
    this.world.broadphase = new CANNON.SAPBroadphase(this.world);
    this.world.allowSleep = true; // Magavad kehad ei arvuteta

    // Kontaktmaterjal (hõõrdumine ja põrkumine)
    const defaultMaterial = new CANNON.Material('default');
    const contactMaterial = new CANNON.ContactMaterial(
      defaultMaterial, defaultMaterial,
      { friction: 0.3, restitution: 0.4 }
    );
    this.world.addContactMaterial(contactMaterial);
    this.world.defaultContactMaterial = contactMaterial;
  }

  addBox(mesh: THREE.Mesh, mass: number, size: THREE.Vector3): CANNON.Body {
    const halfExtents = new CANNON.Vec3(size.x / 2, size.y / 2, size.z / 2);
    const body = new CANNON.Body({
      mass,
      shape: new CANNON.Box(halfExtents),
      position: new CANNON.Vec3(mesh.position.x, mesh.position.y, mesh.position.z),
    });
    this.world.addBody(body);
    this.objects.push({ body, mesh });
    return body;
  }

  addSphere(mesh: THREE.Mesh, mass: number, radius: number): CANNON.Body {
    const body = new CANNON.Body({
      mass,
      shape: new CANNON.Sphere(radius),
      position: new CANNON.Vec3(mesh.position.x, mesh.position.y, mesh.position.z),
    });
    this.world.addBody(body);
    this.objects.push({ body, mesh });
    return body;
  }

  update(delta: number): void {
    this.world.step(1 / 60, delta, 3);

    for (const obj of this.objects) {
      obj.mesh.position.copy(obj.body.position as any);
      obj.mesh.quaternion.copy(obj.body.quaternion as any);
    }
  }

  removeObject(body: CANNON.Body): void {
    const index = this.objects.findIndex(o => o.body === body);
    if (index >= 0) {
      this.world.removeBody(body);
      this.objects.splice(index, 1);
    }
  }
}

🎮 Samm 3 — 3D Mängumootori Struktuur

Engine.ts — Mootori tuumik
class GameEngine {
  private scene: THREE.Scene;
  private camera: THREE.PerspectiveCamera;
  private renderer: THREE.WebGLRenderer;
  private physics: PhysicsManager;
  private clock: THREE.Clock;
  private entities: Map<string, GameEntity> = new Map();
  private systems: GameSystem[] = [];
  private isRunning = false;

  constructor(container: HTMLElement) {
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.shadowMap.enabled = true;
    container.appendChild(this.renderer.domElement);

    this.physics = new PhysicsManager();
    this.clock = new THREE.Clock();

    this.setupDefaultLighting();
  }

  private setupDefaultLighting(): void {
    this.scene.add(new THREE.AmbientLight(0x404080, 0.5));
    const sun = new THREE.DirectionalLight(0xffffff, 1);
    sun.position.set(10, 20, 10);
    sun.castShadow = true;
    this.scene.add(sun);
  }

  addEntity(entity: GameEntity): void {
    this.entities.set(entity.id, entity);
    if (entity.mesh) this.scene.add(entity.mesh);
  }

  removeEntity(id: string): void {
    const entity = this.entities.get(id);
    if (entity) {
      if (entity.mesh) this.scene.remove(entity.mesh);
      entity.destroy();
      this.entities.delete(id);
    }
  }

  addSystem(system: GameSystem): void {
    this.systems.push(system);
    this.systems.sort((a, b) => a.priority - b.priority);
  }

  start(): void {
    this.isRunning = true;
    this.gameLoop();
  }

  private gameLoop(): void {
    if (!this.isRunning) return;
    requestAnimationFrame(() => this.gameLoop());

    const delta = this.clock.getDelta();

    // Update süsteemid
    for (const system of this.systems) {
      system.update(this.entities, delta);
    }

    // Füüsika
    this.physics.update(delta);

    // Render
    this.renderer.render(this.scene, this.camera);
  }
}

// Kasutamine:
// const engine = new GameEngine(document.body);
// engine.addSystem(new InputSystem());
// engine.addSystem(new MovementSystem());
// engine.addSystem(new CameraSystem());
// engine.start();

📝 Harjutused

  • Füüsikamootor integratsioon

    Seadista Three.js + Cannon-es. Loo põrand ja 20 kuubikut mis kukuvad ja põrkavad. Lisa materjalide hõõrdumine ja põrkumine. Debug wireframe.

  • PhysicsManager klass

    Loo PhysicsManager: addBox, addSphere, removeObject, raycast. Sünkroniseeri automaatselt Cannon kehad ja Three meshid. Lisa kontaktmaterjalid.

  • 3D mängija kontroller füüsikaga

    Loo mängija kontroller: WASD liikumine jõududega, hüppamine impulssiga, maapinna tuvastamine raycast'iga. Kaamera jälgib sujuvalt (lerp).

  • GameEngine klass

    Loo mängumootori tuumik: Entity haldus, System pipeline, füüsika integratsioon, renderdamine. Lisai start/stop/pause. FPS counter.

  • Kokkupõrkereaktsioonid

    Kokkupõrke sündmused: collision callback. Kui kuubik tabab teist → heliefekt + värvimuutus + partiklid + score. Lisa trigger zone'id.

  • 3D platformer prototüüp

    Ehita 3D platformer: liikuvad platvormid, hüppamine, kogutavad mündid, ohtlikud alad. 3 taset, iga tase keerulisem. Checkpoint süsteem.

  • Vehicle physics

    Loo sõiduk Cannon-es RaycastVehicle'iga: 4 ratast, gaas/pidur, roolimine. Lisa rambi ja hüpped. Lihtne rada koonustega.

← Moodul 10b Moodul 11b: 3D Mudelid →