Creating a playable scene
Primitive third-person player example
In this example, we will reflect the minimum important concepts and knowledge to understand how to work with the library.
Writing a custom module
We need to have a centralized way to determine which key is pressed. For this, CoreContextModule
is a great fit.
We will subscribe to the necessary DOM events in useCtx
and unsubscribe in the returned cleanup function.
It will allow us to elegantly and centrally determine which button is pressed, which we will use in other features.
import * as KVY from "@vladkrutenyuk/three-kvy-core";
export class InputKeyModule extends KVY.CoreContextModule {
keys = new Set();
isKeyDown = (key) => this.keys.has(key);
useCtx(ctx) {
const onKeyDown = (e) => this.keys.add(e.code);
const onKeyUp = (e) => this.keys.delete(e.code);
const onBlur = () => this.keys.clear();
const dom = ctx.three.renderer.domElement;
dom.addEventListener("keydown", onKeyDown);
dom.addEventListener("keyup", onKeyUp);
dom.addEventListener("blur", onBlur);
return () => {
dom.removeEventListener("keydown", onKeyDown);
dom.removeEventListener("keyup", onKeyUp);
dom.removeEventListener("blur", onBlur);
}
}
}
Create a player using features
Movement
Now let's create a feature SimpleMovement
, which will move the object to which we add it based on the pressed keys.
We will also use props
in the constructor to demonstrate how to pass props into features.
import * as KVY from "@vladkrutenyuk/three-kvy-core";
export class SimpleMovement extends KVY.Object3DFeature {
speed = 10;
constructor(object, props) {
super(object);
this.speed = props.speed;
}
onBeforeRender(ctx) {
const offset = this.speed * ctx.deltaTime;
const pos = this.object.position;
const key = ctx.modules.input.isKeyDown;
if (key('KeyW')) pos.z -= offset;
if (key('KeyS')) pos.z += offset;
if (key('KeyD')) pos.x += offset;
if (key('KeyA')) pos.x -= offset;
}
}
- We use the lifecycle method
onBeforeRender
, which is called every time before the render call. onBeforeRender
provides us with thectx
argument, through which we can access various properties and modules that we have attached.- We take
ctx.deltaTime
to correctly calculate the position offset. - We access the object to which the feature is attached through
this.object
and change its position.
Camera follows player
Now we need the camera to follow the player as we move. Let's create a simple feature that attaches the camera to the object and positions it according to the specified parameters.
import * as KVY from "@vladkrutenyuk/three-kvy-core";
import * as THREE from "three";
export class CameraFollow extends KVY.Object3DFeature {
constructor(object, props) {
super(object);
this.offset = props.offset;
this.lookAt = props.lookAt;
}
useCtx(ctx) {
const camera = ctx.three.camera;
camera.position.fromArray(this.offset);
camera.lookAt(new THREE.Vector3().fromArray(this.lookAt));
this.object.add(camera);
return () => {
this.object.remove(camera);
}
}
}
Graphics for player
We can also put the initialization of three.js graphics for the player into a separate feature to separate the code and conveniently automatically clean up memory when the feature is destroyed.
export class PlayerGraphics extends KVY.Object3DFeature {
constructor(object) {
super(object);
const capsule = new THREE.Mesh(new THREE.CapsuleGeometry(0.5), new THREE.MeshNormalMaterial());
capsule.position.y = 1;
const axes = new THREE.AxesHelper(5);
axes.position.y = 0.1;
this.components = [capsule, axes];
this.object.add(...this.components);
}
onDestroy() {
for (const component of this.components) {
component.removeFromParent();
component.geometry.dispose();
component.material.dispose();
}
}
}
Finally, build a scene!
- Initialize the context
ctx
(CoreContext
) with theInputKeyModule
module under the keyinput
. - Mount the context
ctx.three.mount
to the specified HTML element, thus the three.js rendering canvas will be added to this element. If something, all the logic for resizing, etc. is already present inside, so we don't need to worry about it. And start the animation loop withctx.run()
. - Create a 3D object
player
and add the features we wrote to it using the static factory methodaddFeature
. It is important to specify the class (constructor) of the feature, not the instance. - Add it to
ctx.root
, so the context is attached to the features we added to it. - Set up the external appearance of the scene to make it easier to see what is happening.
import * as KVY from "@vladkrutenyuk/three-kvy-core";
import * as THREE from "three";
import { InputKeyModule } from "./InputKeyModule.js";
import { SimpleMovement } from "./SimpleMovement.js";
import { CameraFollow } from "./CameraFollow.js";
import { PlayerGraphics } from "./PlayerGraphics.js";
const ctx = KVY.CoreContext.create(THREE,
{ input: new InputKeyModule() },
{ renderer: { antialias: true } },
);
ctx.three.mount(document.querySelector("#canvas-container"));
ctx.run();
// player
const player = new THREE.Group();
KVY.addFeature(player, SimpleMovement, { speed: 6 });
KVY.addFeature(player, CameraFollow, { offset: [0, 4, 5], lookAt: [0, 1.5, 0] });
KVY.addFeature(player, PlayerGraphics);
ctx.root.add(player);
// scene graphics
const scene = ctx.three.scene;
const bgColor = 0x202020;
scene.background = new THREE.Color(bgColor);
scene.fog = new THREE.Fog(bgColor, 8, 30);
scene.add(new THREE.GridHelper(100, 100));