cleanup, name tag

This commit is contained in:
Evert Prants 2023-06-18 18:51:09 +03:00
parent 19ea3d22ab
commit d25086632f
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
19 changed files with 337 additions and 34 deletions

View File

@ -15,8 +15,8 @@ export class ThirdPersonCamera {
private prevMousePos = new Vector2();
private angleAroundPlayer = 180;
private pitch = -45;
private distance = 6;
private pitch = -24;
private distance = 8;
private panning = false;
private pinching = false;
@ -25,10 +25,14 @@ export class ThirdPersonCamera {
private _moveFn?: (x: number, y: number) => void;
public minZoom = 2;
public maxZoom = 32;
constructor(
private camera: PerspectiveCamera,
private target: Object3D,
private eventTarget: HTMLElement
private eventTarget: HTMLElement,
private cameraOrignOffset = new Vector3(0, 4.75, 0)
) {}
private dragEvent = (x: number, y: number) => {
@ -44,12 +48,12 @@ export class ThirdPersonCamera {
this.angleAroundPlayer + ((offset.x * 0.3) % 360);
}
this.pitch = clamp(this.pitch + offset.y * 0.3, -90, 90);
this.pitch = clamp(this.pitch + offset.y * 0.3, -89.9, 89.9);
this.calculateCameraOffset();
}
};
events: Record<string, (...arg: any[]) => void> = {
private events: Record<string, (...arg: any[]) => void> = {
contextmenu: (e: MouseEvent) => e.preventDefault(),
mousedown: (e: MouseEvent) => {
if (e.button === 2) {
@ -76,7 +80,7 @@ export class ThirdPersonCamera {
mousemove: (e: MouseEvent) => this.dragEvent(e.clientX, e.clientY),
wheel: (e: WheelEvent) => {
e.deltaY < 0 ? (this.distance /= 1.2) : (this.distance *= 1.2);
this.distance = clamp(this.distance, 3, 20);
this.distance = clamp(this.distance, this.minZoom, this.maxZoom);
this.calculateCameraOffset();
},
// mobile
@ -102,7 +106,7 @@ export class ThirdPersonCamera {
if (this.previousPinchLength) {
const delta = pinchLength / this.previousPinchLength;
delta > 0 ? (this.distance *= delta) : (this.distance /= delta);
this.distance = clamp(this.distance, 3, 20);
this.distance = clamp(this.distance, this.minZoom, this.maxZoom);
this.calculateCameraOffset();
}
@ -125,7 +129,9 @@ export class ThirdPersonCamera {
initialize() {
Object.keys(this.events).forEach((key) => {
this.eventTarget.addEventListener(key, this.events[key]);
this.eventTarget.addEventListener(key, this.events[key], {
passive: false,
});
});
this.calculateCameraOffset();
}
@ -171,12 +177,13 @@ export class ThirdPersonCamera {
private getTargetOffset(): Vector3 {
const offset = this.offsetFromPlayer.clone();
offset.add(this.cameraOrignOffset);
offset.add(this.target.position);
return offset;
}
private getTargetLookAt(): Vector3 {
const offset = new Vector3(0, 4.75, 0);
const offset = this.cameraOrignOffset.clone();
offset.add(this.target.position);
return offset;
}

View File

@ -34,9 +34,9 @@ export class Game extends Engine {
}
async loadLevel(path: string) {
this.events.emit('reset');
const data = await assetManager.loadJsonData(path);
await this.getComponent(LevelComponent).deserializeLevelSave(data);
this.events.emit('initialized');
}
}

View File

@ -8,8 +8,7 @@ import {
} from '@freeblox/engine';
import { GameEvents } from '../types/events';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { Object3D, Vector3 } from 'three';
import { Vector3 } from 'three';
import { ThirdPersonCamera } from './camera';
/**
@ -17,7 +16,6 @@ import { ThirdPersonCamera } from './camera';
*/
export class GameplayComponent extends EngineComponent {
public name = GameplayComponent.name;
public events = new EventEmitter<GameEvents>();
public world!: World;
public cleanUpEvents?: () => void;
@ -39,7 +37,6 @@ export class GameplayComponent extends EngineComponent {
override initialize(): void {
this.world = this.renderer.scene.getObjectByName('World') as World;
this.cleanUpEvents = this.bindEvents();
this.loadCharacter('Diamond');
}
override update(delta: number): void {
@ -67,7 +64,7 @@ export class GameplayComponent extends EngineComponent {
}
}
override cleanUp(): void {
override dispose(): void {
this.cleanUpEvents?.();
}
@ -122,11 +119,17 @@ export class GameplayComponent extends EngineComponent {
}
};
const initializedEvent = () => {
this.loadCharacter('Diamond');
};
window.addEventListener('keydown', keyDownEvent);
window.addEventListener('keyup', keyUpEvent);
this.events.addListener('initialized', initializedEvent);
return () => {
window.removeEventListener('keydown', keyDownEvent);
window.removeEventListener('keyup', keyUpEvent);
this.events.removeEventListener('initialized', initializedEvent);
};
}
}

View File

@ -33,7 +33,7 @@ export class HistoryComponent extends EngineComponent {
update(delta: number): void {}
cleanUp(): void {
dispose(): void {
this.cleanUpEvents?.call(this);
this.history.length = 0;
this.restory.length = 0;

View File

@ -21,7 +21,7 @@ export class ShortcutsComponent extends EngineComponent {
update(delta: number): void {}
cleanUp(): void {
dispose(): void {
this.cleanUpEvents?.call(this);
}

View File

@ -90,7 +90,7 @@ export class WorkspaceComponent extends EngineComponent {
this.viewHelper?.render(this.renderer.renderer);
}
cleanUp(): void {
dispose(): void {
this.removeFromScene(this.renderer.scene);
this.cleanUpEvents?.call(this);
this.viewHelper?.dispose();

View File

@ -0,0 +1 @@
export * from './utils';

View File

@ -0,0 +1,227 @@
import { CanvasTexture, LinearFilter, ClampToEdgeWrapping } from 'three';
export function rgbToHex(r: number, g: number, b: number): number {
return (1 << 24) + (r << 16) + (g << 8) + b;
}
type RadiusQuad = { tl: number; tr: number; br: number; bl: number };
export interface CanvasUtilsOptions {
fill?: boolean;
backgroundColor?: string;
foregroundColor?: string;
rounded?: boolean;
roundedRadius?: number;
textBorderColor?: string;
textBorderSize?: number;
textShadowBlur?: number;
}
export class CanvasUtils {
constructor(private options?: CanvasUtilsOptions) {}
public createTextCanvas(
text: string | string[],
bold = true,
fontSize = 16,
padding = 4
): { texture: CanvasTexture; width: number; height: number } {
const ctx = document.createElement('canvas').getContext('2d')!;
const font = `${fontSize}px${bold ? ' bold' : ''} sans`;
const lines = Array.isArray(text) ? text : [text];
const lineWidths: number[] = [];
let longestLine = 0;
// Measure the text bounds
ctx.font = font;
lines.forEach((line) => {
const lineWidth = ctx.measureText(line).width;
if (longestLine < lineWidth) {
longestLine = lineWidth;
}
lineWidths.push(lineWidth);
});
const width = longestLine + padding * 2;
const textHeight = fontSize * lines.length;
const height = textHeight + padding * 2;
// Resize canvas
ctx.canvas.width = width;
ctx.canvas.height = height;
// Set text parameters
ctx.font = font;
ctx.textAlign = 'center';
// Draw background
if (this.options?.fill ?? true) {
ctx.fillStyle = this.options?.backgroundColor || '#fff';
if (this.options?.rounded) {
CanvasUtils.roundRect(
ctx,
0,
0,
width,
height,
this.options?.roundedRadius || 4,
true,
false
);
} else {
ctx.fillRect(0, 0, width, height);
}
}
// Scale the text to fit within the canvas
const scaleFactor = Math.min(1, width / longestLine);
ctx.translate(
Math.floor(width / 2 - padding) + 0.5,
Math.floor(padding + fontSize / 2) + 0.5
);
ctx.scale(scaleFactor, 1);
// Draw the text
if (this.options?.textShadowBlur !== undefined) {
ctx.shadowColor = this.options?.textBorderColor || '#000';
ctx.shadowBlur = this.options?.textShadowBlur;
if (this.options?.textBorderSize !== undefined) {
ctx.lineWidth = this.options?.textBorderSize;
lines.forEach((line, i) => {
ctx.strokeText(line, padding, i * fontSize + padding);
});
}
ctx.shadowBlur = 0;
}
ctx.fillStyle = this.options?.foregroundColor || '#000';
lines.forEach((line, i) => {
ctx.fillText(line, padding, i * fontSize + padding);
});
// Create texture with appropriate flags
const texture = new CanvasTexture(ctx.canvas);
texture.minFilter = LinearFilter;
texture.wrapS = ClampToEdgeWrapping;
texture.wrapT = ClampToEdgeWrapping;
return { texture, width, height };
}
/**
* Draws a rounded rectangle using the current state of the canvas.
* If you omit the last three params, it will draw a rectangle
* outline with a 5 pixel border radius
*
* https://stackoverflow.com/a/3368118
*
* @param {CanvasRenderingContext2D} ctx
* @param {Number} x The top left x coordinate
* @param {Number} y The top left y coordinate
* @param {Number} width The width of the rectangle
* @param {Number} height The height of the rectangle
* @param {Number} [radius = 5] The corner radius; It can also be an object
* to specify different radii for corners
* @param {Number} [radius.tl = 0] Top left
* @param {Number} [radius.tr = 0] Top right
* @param {Number} [radius.br = 0] Bottom right
* @param {Number} [radius.bl = 0] Bottom left
* @param {Boolean} [fill = false] Whether to fill the rectangle.
* @param {Boolean} [stroke = true] Whether to stroke the rectangle.
*/
public static roundRect(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
radius?: number | RadiusQuad,
fill?: boolean,
stroke?: boolean
) {
if (typeof stroke === 'undefined') {
stroke = true;
}
if (typeof radius === 'undefined') {
radius = 5;
}
if (typeof radius === 'number') {
radius = <RadiusQuad>{ tl: radius, tr: radius, br: radius, bl: radius };
} else {
const defaultRadius = <RadiusQuad>{ tl: 0, tr: 0, br: 0, bl: 0 };
for (let side in defaultRadius) {
const sideKey = side as keyof RadiusQuad;
radius[sideKey] = radius[sideKey] || defaultRadius[sideKey];
}
}
ctx.beginPath();
ctx.moveTo(x + radius.tl, y);
ctx.lineTo(x + width - radius.tr, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
ctx.lineTo(x + width, y + height - radius.br);
ctx.quadraticCurveTo(
x + width,
y + height,
x + width - radius.br,
y + height
);
ctx.lineTo(x + radius.bl, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
ctx.lineTo(x, y + radius.tl);
ctx.quadraticCurveTo(x, y, x + radius.tl, y);
ctx.closePath();
if (fill) {
ctx.fill();
}
if (stroke) {
ctx.stroke();
}
}
public static readPixelDataRGB(image: HTMLImageElement): number[] {
const array = new Array(image.width * image.height);
const ctx = document.createElement('canvas').getContext('2d')!;
ctx.canvas.width = image.width;
ctx.canvas.height = image.height;
ctx.drawImage(image, 0, 0, image.width, image.height);
// pixel data
const data = ctx.getImageData(0, 0, image.width, image.height);
for (let x = 0; x < image.width; x++) {
for (let y = 0; y < image.height; y++) {
const index = x + y * image.width;
array[index] = rgbToHex(
data.data[index * 4],
data.data[index * 4 + 1],
data.data[index * 4 + 2]
);
}
}
return array;
}
public static readPixelDataRScaled(
image: HTMLImageElement,
scale: number
): number[] {
const array = new Array(image.width * image.height);
const ctx = document.createElement('canvas').getContext('2d')!;
ctx.canvas.width = image.width;
ctx.canvas.height = image.height;
ctx.drawImage(image, 0, 0, image.width, image.height);
// pixel data
const data = ctx.getImageData(0, 0, image.width, image.height);
for (let x = 0; x < image.width; x++) {
for (let y = 0; y < image.height; y++) {
const index = x + y * image.width;
array[index] = (data.data[index * 4] * scale) / 255;
}
}
return array;
}
}

View File

@ -38,7 +38,7 @@ export class EnvironmentComponent extends EngineComponent {
update(delta: number): void {}
cleanUp(): void {
dispose(): void {
this.renderer.scene.remove(this.ambient);
this.renderer.scene.remove(this.directional);
this.cleanUpEvents?.call(this);

View File

@ -48,7 +48,7 @@ export class LevelComponent extends EngineComponent {
update(delta: number): void {}
cleanUp(): void {
dispose(): void {
this.cleanUpEvents?.call(this);
}
@ -95,7 +95,6 @@ export class LevelComponent extends EngineComponent {
public async deserializeLevelSave(save: WorldFile) {
// Reset the world
this.events.emit('reset');
assetManager.freeAll();
// Load all assets
await assetManager.loadAll(save.assets);

View File

@ -49,7 +49,7 @@ export class MouseComponent extends EngineComponent {
this.mouseButtonsLast = [...this.mouseButtons];
}
cleanUp(): void {
dispose(): void {
this.cleanUpEvents?.call(this);
}

View File

@ -33,7 +33,7 @@ export class ViewportComponent extends EngineComponent {
update(dt: number) {}
cleanUp(): void {
dispose(): void {
this.cleanUpEvents?.call(this);
}

View File

@ -8,7 +8,7 @@ import { Renderer } from './renderer';
export class Engine extends GameRunner {
public lastTick = performance.now();
public running = false;
public events = new EventEmitter<EngineEvents>();
public events!: EventEmitter<EngineEvents>;
public render!: Renderer;
public element!: HTMLElement;
public components: EngineComponent[] = [];
@ -31,7 +31,7 @@ export class Engine extends GameRunner {
override stop(): void {
this.running = false;
for (const component of this.components) {
component.cleanUp();
component.dispose();
}
}

View File

@ -30,7 +30,7 @@ export class Renderer {
this.renderer.render(this.scene, this.camera);
}
cleanUp() {
dispose() {
this.renderer.dispose();
}
}

View File

@ -12,6 +12,8 @@ import { Ticking } from '../types/ticking';
import { EditorProperty } from '../decorators/property';
import { MeshPart } from './mesh.object';
import { clamp } from 'three/src/math/MathUtils.js';
import { NameTag } from './nametag.object';
import { CanvasUtils } from '../canvas/utils';
export type HumanoidBodyPart =
| 'Head'
@ -53,10 +55,20 @@ export class Humanoid extends GameObject implements Ticking {
public static bodyAnimationNames = ['Idle', 'Walk'];
public static nameTagBuilder = new CanvasUtils({
fill: false,
textBorderColor: '#000',
foregroundColor: '#fff',
textShadowBlur: 2,
textBorderSize: 1,
});
private mixer!: AnimationMixer;
private idleAction!: AnimationAction;
private walkAction!: AnimationAction;
private nameTag?: NameTag;
@EditorProperty({ type: Number })
get health() {
return this._health;
@ -114,6 +126,8 @@ export class Humanoid extends GameObject implements Ticking {
this.idleAction = this.mixer.clipAction(idleClip);
this.walkAction = this.mixer.clipAction(walkClip);
this.idleAction.play();
this.createNameTag();
}
setVelocity(velocity: Vector3) {
@ -173,4 +187,14 @@ export class Humanoid extends GameObject implements Ticking {
this.health = 0;
this.detach();
}
private createNameTag() {
if (this.nameTag) {
this.nameTag.dispose();
}
this.nameTag = NameTag.create(Humanoid.nameTagBuilder, this.parent!.name);
this.nameTag.position.set(0, 1.5, 0);
this.add(this.nameTag);
}
}

View File

@ -26,6 +26,7 @@ export const instancableGameObjects: Record<string, Instancable<GameObject>> = {
export * from './environment.object';
export * from './world.object';
export * from './nametag.object';
export {
Group,
Cylinder,

View File

@ -0,0 +1,47 @@
import { Sprite, CanvasTexture, SpriteMaterial } from 'three';
import { CanvasUtils } from '../canvas';
import { GameObject } from '..';
export class NameTag extends GameObject {
public objectType = NameTag.name;
public virtual = true;
public archivable = false;
public width!: number;
public height!: number;
private texture!: CanvasTexture;
private material!: SpriteMaterial;
public static create(builder: CanvasUtils, name: string) {
const { texture, width, height } = builder.createTextCanvas(
name,
false,
48
);
const obj = new NameTag();
obj.texture = texture;
obj.width = width;
obj.height = height;
obj.material = new SpriteMaterial({
map: texture,
transparent: true,
});
const label = new Sprite(obj.material);
const labelBaseScale = 0.01;
label.scale.x = width * labelBaseScale;
label.scale.y = height * labelBaseScale;
obj.add(label);
return obj;
}
dispose() {
this.material.dispose();
this.texture.dispose();
}
}

View File

@ -24,5 +24,5 @@ export abstract class EngineComponent {
/**
* Clean up the component
*/
abstract cleanUp(): void;
abstract dispose(): void;
}

View File

@ -1,10 +1,4 @@
import {
AnimationClip,
Bone,
Object3D,
SkeletonHelper,
SkinnedMesh,
} from 'three';
import { AnimationClip, Bone, Object3D, SkinnedMesh } from 'three';
import { assetManager } from '../assets';
import { Group } from '../gameobjects/group.object';
import { MeshPart } from '../gameobjects/mesh.object';