implementing basic character
This commit is contained in:
parent
4c44c7923f
commit
d40e2073ee
|
@ -177,8 +177,10 @@ onMounted(() => createSceneMap());
|
|||
|
||||
<style lang="scss">
|
||||
.sidebar {
|
||||
--sidebar-width: 320px;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
width: 360px;
|
||||
width: var(--sidebar-width);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -62,67 +62,51 @@ const formFields = computed(() => {
|
|||
const fields: FormItem[] = [];
|
||||
|
||||
object.properties
|
||||
.filter((item) => item.definition.exposed && item.definition.name)
|
||||
.filter((item) => item.exposed && item.name)
|
||||
.forEach((property) => {
|
||||
if (
|
||||
property.definition.type === String ||
|
||||
property.definition.type === Number
|
||||
) {
|
||||
if (property.type === String || property.type === Number) {
|
||||
fields.push({
|
||||
name: property.definition.name!,
|
||||
label: toCapitalizedWords(property.definition.name!),
|
||||
value: (object as unknown as Record<string, unknown>)[
|
||||
property.definition.name!
|
||||
],
|
||||
type: property.definition.type === String ? 'string' : 'number',
|
||||
name: property.name,
|
||||
label: toCapitalizedWords(property.name),
|
||||
value: (object as unknown as Record<string, unknown>)[property.name],
|
||||
type: property.type === String ? 'string' : 'number',
|
||||
component: Field,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
property.definition.type === Vector3 ||
|
||||
property.definition.type === Euler
|
||||
) {
|
||||
if (property.type === Vector3 || property.type === Euler) {
|
||||
fields.push({
|
||||
name: property.definition.name!,
|
||||
label: toCapitalizedWords(property.definition.name!),
|
||||
value: (object as unknown as Record<string, unknown>)[
|
||||
property.definition.name!
|
||||
],
|
||||
type: property.definition.type === Vector3 ? 'vector' : 'euler',
|
||||
name: property.name,
|
||||
label: toCapitalizedWords(property.name),
|
||||
value: (object as unknown as Record<string, unknown>)[property.name],
|
||||
type: property.type === Vector3 ? 'vector' : 'euler',
|
||||
component: Vector3Field,
|
||||
});
|
||||
}
|
||||
|
||||
if (property.definition.type === Color) {
|
||||
if (property.type === Color) {
|
||||
fields.push({
|
||||
name: property.definition.name!,
|
||||
label: toCapitalizedWords(property.definition.name!),
|
||||
value: (object as unknown as Record<string, unknown>)[
|
||||
property.definition.name!
|
||||
],
|
||||
name: property.name,
|
||||
label: toCapitalizedWords(property.name),
|
||||
value: (object as unknown as Record<string, unknown>)[property.name],
|
||||
component: ColorPicker,
|
||||
});
|
||||
}
|
||||
|
||||
if (property.definition.type === Boolean) {
|
||||
if (property.type === Boolean) {
|
||||
fields.push({
|
||||
name: property.definition.name!,
|
||||
label: toCapitalizedWords(property.definition.name!),
|
||||
value: (object as unknown as Record<string, unknown>)[
|
||||
property.definition.name!
|
||||
],
|
||||
name: property.name,
|
||||
label: toCapitalizedWords(property.name),
|
||||
value: (object as unknown as Record<string, unknown>)[property.name],
|
||||
component: Checkbox,
|
||||
});
|
||||
}
|
||||
|
||||
if (property.definition.type === AssetInfo) {
|
||||
if (property.type === AssetInfo) {
|
||||
fields.push({
|
||||
name: property.definition.name!,
|
||||
label: toCapitalizedWords(property.definition.name!),
|
||||
value: (object as unknown as Record<string, unknown>)[
|
||||
property.definition.name!
|
||||
],
|
||||
name: property.name,
|
||||
label: toCapitalizedWords(property.name),
|
||||
value: (object as unknown as Record<string, unknown>)[property.name],
|
||||
component: AssetPicker,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ class CameraControls extends EventDispatcher {
|
|||
public pointerSpeed = 1.5;
|
||||
public panSpeed = 1;
|
||||
public movementSpeed = 0.025;
|
||||
public shiftMultiplier = 2;
|
||||
public shiftMultiplier = 0.45;
|
||||
public zoomScale = 100;
|
||||
public screenSpacePanning = true;
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ import {
|
|||
Renderer,
|
||||
LevelComponent,
|
||||
WorldFile,
|
||||
instanceCharacterObject,
|
||||
Humanoid,
|
||||
} from '@freeblox/engine';
|
||||
import { EditorEvents } from '../types/events';
|
||||
import { WorkspaceComponent } from './workspace';
|
||||
|
@ -27,6 +29,8 @@ export class Editor extends GameRunner {
|
|||
public level!: LevelComponent;
|
||||
public running = false;
|
||||
|
||||
private test!: Humanoid;
|
||||
|
||||
override mount(element: HTMLElement) {
|
||||
this.element = element;
|
||||
this.render = new Renderer(element);
|
||||
|
@ -55,6 +59,12 @@ export class Editor extends GameRunner {
|
|||
|
||||
this.viewport.setSizeFromViewport();
|
||||
this.start();
|
||||
|
||||
instanceCharacterObject().then((obj) => {
|
||||
this.workspace.world.add(obj);
|
||||
this.test = obj.getObjectByName('Humanoid');
|
||||
this.test.initialize();
|
||||
});
|
||||
}
|
||||
|
||||
override loop(now: DOMHighResTimeStamp) {
|
||||
|
@ -66,6 +76,7 @@ export class Editor extends GameRunner {
|
|||
this.viewport.update(delta);
|
||||
this.workspace.update(delta);
|
||||
this.mouse.update(delta);
|
||||
this.test?.tick(delta);
|
||||
|
||||
this.render.render();
|
||||
this.workspace.render();
|
||||
|
|
|
@ -305,7 +305,7 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
this.selection.length = 0;
|
||||
|
||||
filteredSelection.forEach((entry) => {
|
||||
const newObject = this.cutOperation ? entry : entry.clone();
|
||||
const newObject = entry.clone();
|
||||
this.world.add(newObject);
|
||||
this.selection.push(newObject);
|
||||
});
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { CubeTexture, CubeTextureLoader, Texture, TextureLoader } from 'three';
|
||||
import { Asset, AssetsEvents } from '../types/asset';
|
||||
import { Asset, AssetType, AssetsEvents } from '../types/asset';
|
||||
import { EventEmitter } from '../utils/events';
|
||||
import { GLTF, GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
|
||||
export class AssetManagerFactory extends EventEmitter<AssetsEvents> {
|
||||
public assets: Asset[] = [];
|
||||
public texureLoader = new TextureLoader();
|
||||
public cubeTextureLoader = new CubeTextureLoader();
|
||||
public gltfLoader = new GLTFLoader();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -28,11 +30,7 @@ export class AssetManagerFactory extends EventEmitter<AssetsEvents> {
|
|||
* @param name Asset name
|
||||
* @param type Texture type
|
||||
*/
|
||||
async createAsset(
|
||||
data: string,
|
||||
name: string,
|
||||
type: 'Texture' | 'CubeTexture' = 'Texture'
|
||||
) {
|
||||
async createAsset(data: string, name: string, type: AssetType = 'Texture') {
|
||||
const asset: Asset = {
|
||||
name,
|
||||
type,
|
||||
|
@ -41,18 +39,36 @@ export class AssetManagerFactory extends EventEmitter<AssetsEvents> {
|
|||
|
||||
await this.load(asset);
|
||||
|
||||
asset.path = `fblxassetid:${asset.texture!.uuid}`;
|
||||
asset.path = `fblxassetid:${(asset.texture || asset.object)!.uuid}`;
|
||||
this.assets.push(asset);
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset into a Texture
|
||||
* Load asset into a Texture or Model
|
||||
* @param asset Remote or local asset data
|
||||
*/
|
||||
async load(asset: Asset) {
|
||||
this.emit('loadStart', asset);
|
||||
|
||||
if (asset.type === 'Mesh') {
|
||||
const data = await this.loadMeshData(
|
||||
asset.remote ? asset.path : asset.data,
|
||||
asset.name
|
||||
).catch((error) => {
|
||||
this.emit('loadError', error);
|
||||
throw error;
|
||||
});
|
||||
|
||||
asset.animations = data.animations;
|
||||
asset.object = data.scene;
|
||||
|
||||
this.emit('loadComplete', asset);
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
return (
|
||||
asset.type === 'Texture'
|
||||
? this.loadTextureData(
|
||||
|
@ -119,7 +135,7 @@ export class AssetManagerFactory extends EventEmitter<AssetsEvents> {
|
|||
* @param path Path
|
||||
* @param name Texture name
|
||||
*/
|
||||
private async loadTextureData(path: string, name?: string) {
|
||||
async loadTextureData(path: string, name?: string) {
|
||||
return new Promise<Texture>((resolve, reject) => {
|
||||
this.texureLoader.load(
|
||||
path,
|
||||
|
@ -138,7 +154,7 @@ export class AssetManagerFactory extends EventEmitter<AssetsEvents> {
|
|||
* @param path pos-x, neg-x, pos-y, neg-y, pos-z, neg-z
|
||||
* @param name Texture name
|
||||
*/
|
||||
private async loadCubeTexture(path: string[], name?: string) {
|
||||
async loadCubeTexture(path: string[], name?: string) {
|
||||
return new Promise<CubeTexture>((resolve, reject) => {
|
||||
this.cubeTextureLoader.load(
|
||||
path,
|
||||
|
@ -151,6 +167,12 @@ export class AssetManagerFactory extends EventEmitter<AssetsEvents> {
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
async loadMeshData(path: string, name?: string) {
|
||||
return new Promise<GLTF>((resolve, reject) => {
|
||||
this.gltfLoader.load(path, (entry) => resolve(entry), undefined, reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const assetManager = new AssetManagerFactory();
|
||||
|
|
|
@ -27,7 +27,7 @@ export class ViewportComponent extends EngineComponent {
|
|||
|
||||
initialize() {
|
||||
this.cleanUpEvents = this.bindEvents();
|
||||
this.camera.position.set(16, 8, 16);
|
||||
this.camera.position.set(0, 4, 8);
|
||||
}
|
||||
|
||||
update(dt: number) {}
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'reflect-metadata';
|
|||
import { Property, PropertyDefinition } from '../types/property';
|
||||
|
||||
export function EditorProperty(
|
||||
editorPropertyDefinition?: PropertyDefinition
|
||||
editorPropertyDefinition?: Omit<PropertyDefinition, 'name'>
|
||||
): PropertyDecorator {
|
||||
return (target, propertyKey): void => {
|
||||
let properties: Array<Property> = Reflect.getOwnMetadata(
|
||||
|
|
|
@ -14,8 +14,6 @@ import { EditorProperty } from '../decorators/property';
|
|||
export class Brick extends GameObject3D {
|
||||
public objectType = Brick.name;
|
||||
private texturePath?: string;
|
||||
protected material = new MeshPhongMaterial();
|
||||
protected mesh: Mesh = new Mesh(this.geometry, this.material);
|
||||
|
||||
@EditorProperty({ type: Color })
|
||||
get color() {
|
||||
|
@ -68,7 +66,9 @@ export class Brick extends GameObject3D {
|
|||
public mass = 1;
|
||||
|
||||
constructor(
|
||||
protected geometry: BufferGeometry = gameObjectGeometries.boxGeometry
|
||||
protected geometry: BufferGeometry = gameObjectGeometries.boxGeometry,
|
||||
protected material = new MeshPhongMaterial(),
|
||||
protected mesh: Mesh = new Mesh(geometry, material)
|
||||
) {
|
||||
super();
|
||||
this.name = this.objectType;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { Mesh } from 'three';
|
||||
import { Brick } from './brick.object';
|
||||
import { gameObjectGeometries } from './geometries';
|
||||
|
||||
export class Capsule extends Brick {
|
||||
public objectType = Capsule.name;
|
||||
protected mesh = new Mesh(this.geometry, this.material);
|
||||
|
||||
constructor() {
|
||||
super(gameObjectGeometries.capsuleGeometry);
|
||||
this.name = this.objectType;
|
||||
}
|
||||
}
|
|
@ -1,46 +1,73 @@
|
|||
import { BoxGeometry, CylinderGeometry, SphereGeometry } from 'three';
|
||||
import {
|
||||
BoxGeometry,
|
||||
CapsuleGeometry,
|
||||
CylinderGeometry,
|
||||
SphereGeometry,
|
||||
TorusGeometry,
|
||||
} from 'three';
|
||||
|
||||
class WedgeGeometry extends BoxGeometry {
|
||||
type = 'WedgeGeometry';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const pos = this.attributes.position;
|
||||
for (let i = 0; i < pos.count; i++) {
|
||||
if (pos.getX(i) < 0 && pos.getY(i) > 0) pos.setY(i, -0.5);
|
||||
}
|
||||
this.computeVertexNormals();
|
||||
}
|
||||
}
|
||||
|
||||
class WedgeCornerGeometry extends BoxGeometry {
|
||||
type = 'WedgeCornerGeometry';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const pos = this.attributes.position;
|
||||
for (let i = 0; i < pos.count; i++) {
|
||||
if (pos.getY(i) > 0 && (pos.getX(i) !== 0.5 || pos.getZ(i) !== -0.5)) {
|
||||
pos.setY(i, -0.5);
|
||||
}
|
||||
}
|
||||
this.computeVertexNormals();
|
||||
}
|
||||
}
|
||||
|
||||
class WedgeInnerCornerGeometry extends BoxGeometry {
|
||||
type = 'WedgeInnerCornerGeometry';
|
||||
constructor() {
|
||||
super();
|
||||
const pos = this.attributes.position;
|
||||
for (let i = 0; i < pos.count; i++) {
|
||||
if (pos.getY(i) > 0 && pos.getX(i) === 0.5 && pos.getZ(i) === 0.5) {
|
||||
pos.setY(i, -0.5);
|
||||
}
|
||||
}
|
||||
this.computeVertexNormals();
|
||||
}
|
||||
}
|
||||
|
||||
class GameObjectGeometryFactory {
|
||||
public boxGeometry = new BoxGeometry();
|
||||
public sphereGeometry = new SphereGeometry(0.5);
|
||||
public cylinderGeometry = new CylinderGeometry(0.5, 0.5);
|
||||
public wedgeGeometry = new BoxGeometry();
|
||||
public wedgeCornerGeometry = new BoxGeometry();
|
||||
public wedgeInnerCornerGeometry = new BoxGeometry();
|
||||
public torusGeometry = new TorusGeometry();
|
||||
public capsuleGeometry = new CapsuleGeometry(1, 1, 8, 16);
|
||||
public wedgeGeometry = new WedgeGeometry();
|
||||
public wedgeCornerGeometry = new WedgeCornerGeometry();
|
||||
public wedgeInnerCornerGeometry = new WedgeInnerCornerGeometry();
|
||||
|
||||
constructor() {
|
||||
this.makeProceduralShapes();
|
||||
this.finalize();
|
||||
}
|
||||
|
||||
private makeProceduralShapes() {
|
||||
const pos = this.wedgeGeometry.attributes.position;
|
||||
for (let i = 0; i < pos.count; i++) {
|
||||
if (pos.getX(i) < 0 && pos.getY(i) > 0) pos.setY(i, -0.5);
|
||||
}
|
||||
this.wedgeGeometry.computeVertexNormals();
|
||||
|
||||
const pos2 = this.wedgeCornerGeometry.attributes.position;
|
||||
for (let i = 0; i < pos2.count; i++) {
|
||||
if (pos2.getY(i) > 0 && (pos2.getX(i) !== 0.5 || pos2.getZ(i) !== -0.5)) {
|
||||
pos2.setY(i, -0.5);
|
||||
}
|
||||
}
|
||||
this.wedgeCornerGeometry.computeVertexNormals();
|
||||
|
||||
const pos3 = this.wedgeInnerCornerGeometry.attributes.position;
|
||||
for (let i = 0; i < pos3.count; i++) {
|
||||
if (pos3.getY(i) > 0 && pos3.getX(i) === 0.5 && pos3.getZ(i) === 0.5) {
|
||||
pos3.setY(i, -0.5);
|
||||
}
|
||||
}
|
||||
this.wedgeInnerCornerGeometry.computeVertexNormals();
|
||||
}
|
||||
|
||||
private finalize() {
|
||||
this.boxGeometry.computeBoundingBox();
|
||||
this.sphereGeometry.computeBoundingBox();
|
||||
this.cylinderGeometry.computeBoundingBox();
|
||||
this.torusGeometry.computeBoundingBox();
|
||||
this.capsuleGeometry.computeBoundingBox();
|
||||
this.wedgeGeometry.computeBoundingBox();
|
||||
this.wedgeCornerGeometry.computeBoundingBox();
|
||||
this.wedgeInnerCornerGeometry.computeBoundingBox();
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
import {
|
||||
AnimationAction,
|
||||
AnimationClip,
|
||||
AnimationMixer,
|
||||
Object3D,
|
||||
Skeleton,
|
||||
SkinnedMesh,
|
||||
} from 'three';
|
||||
import { GameObject } from '../types/game-object';
|
||||
import { Ticking } from '../types/ticking';
|
||||
import { EditorProperty } from '../decorators/property';
|
||||
import { MeshPart } from './mesh.object';
|
||||
|
||||
export type HumanoidBodyPart =
|
||||
| 'Head'
|
||||
| 'Torso'
|
||||
| 'ArmLeft'
|
||||
| 'ArmRight'
|
||||
| 'LegRight'
|
||||
| 'LegLeft';
|
||||
|
||||
export class Humanoid extends GameObject implements Ticking {
|
||||
public isTickingObject = true;
|
||||
public objectType = Humanoid.name;
|
||||
public name = Humanoid.name;
|
||||
private ready = false;
|
||||
private skeleton!: Skeleton;
|
||||
private _health = 100;
|
||||
private _maxHealth = 100;
|
||||
public static bodyPartNames = [
|
||||
'Head',
|
||||
'Torso',
|
||||
'ArmLeft',
|
||||
'ArmRight',
|
||||
'LegRight',
|
||||
'LegLeft',
|
||||
] as HumanoidBodyPart[];
|
||||
|
||||
public static bodyBoneNames = [
|
||||
'BHead',
|
||||
'BTorso',
|
||||
'BArmL',
|
||||
'BArmR',
|
||||
'BLegR',
|
||||
'BLegL',
|
||||
] as string[];
|
||||
|
||||
public static bodyAnimationNames = ['Idle', 'Walk'];
|
||||
|
||||
private mixer!: AnimationMixer;
|
||||
|
||||
@EditorProperty({ type: Number })
|
||||
get health() {
|
||||
return this._health;
|
||||
}
|
||||
set health(value: number) {
|
||||
const health = Math.max(Math.min(Math.floor(value), this.maxHealth), 0);
|
||||
if (health === 0) this.die();
|
||||
this.health = health;
|
||||
}
|
||||
|
||||
@EditorProperty({ type: Number })
|
||||
get maxHealth() {
|
||||
return this._maxHealth;
|
||||
}
|
||||
set maxHealth(value: number) {
|
||||
const maxHealth = Math.floor(value);
|
||||
if (this.health > maxHealth) {
|
||||
this.health = maxHealth;
|
||||
}
|
||||
this._maxHealth = maxHealth;
|
||||
}
|
||||
|
||||
get alive() {
|
||||
return this.health > 0;
|
||||
}
|
||||
|
||||
private get bodyParts() {
|
||||
return Humanoid.bodyPartNames.map((key) => this.getBodyPartByName(key));
|
||||
}
|
||||
|
||||
getBodyPartByName(name: string) {
|
||||
return this.parent?.getObjectByName(name) as MeshPart;
|
||||
}
|
||||
|
||||
getBoneForBodyPart(part: HumanoidBodyPart) {
|
||||
return this.skeleton.getBoneByName(
|
||||
Humanoid.bodyBoneNames[Humanoid.bodyPartNames.indexOf(part)]
|
||||
);
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
if (!this.parent)
|
||||
throw new Error('Cannot initialize Humanoid to empty parent');
|
||||
|
||||
this.skeleton = (this.bodyParts[0]?.getMesh() as SkinnedMesh).skeleton;
|
||||
|
||||
if (!this.skeleton) throw new Error('Could not find Skeleton for Humanoid');
|
||||
this.skeleton.pose();
|
||||
|
||||
this.mixer = new AnimationMixer(this.parent);
|
||||
this.ready = true;
|
||||
|
||||
const clip = AnimationClip.findByName(this.parent.animations, 'Walk');
|
||||
const action = this.mixer.clipAction(clip);
|
||||
action.play();
|
||||
}
|
||||
|
||||
tick(dt: number): void {
|
||||
if (!this.ready) return;
|
||||
this.mixer.update(dt / 1000);
|
||||
}
|
||||
|
||||
detach(bodyPart?: HumanoidBodyPart) {
|
||||
if (!bodyPart) {
|
||||
Humanoid.bodyPartNames.forEach((part) => this.detach(part));
|
||||
return;
|
||||
}
|
||||
|
||||
const part = this.getBodyPartByName(bodyPart);
|
||||
if (!part) return;
|
||||
const partMesh = part.getMesh() as SkinnedMesh;
|
||||
|
||||
if (partMesh.bindMode === 'detached') return;
|
||||
partMesh.bindMode = 'detached';
|
||||
partMesh.updateMatrixWorld(true);
|
||||
|
||||
const bone = this.getBoneForBodyPart(bodyPart);
|
||||
if (!bone) return;
|
||||
bone.removeFromParent();
|
||||
this.skeleton.update();
|
||||
}
|
||||
|
||||
die() {
|
||||
if (!this.alive) return;
|
||||
this.health = 0;
|
||||
this.detach();
|
||||
}
|
||||
}
|
|
@ -7,12 +7,18 @@ import { WedgeInnerCorner } from './wedge-inner-corner.object';
|
|||
import { GameObject } from '../types/game-object';
|
||||
import { Instancable } from '../types/instancable';
|
||||
import { Group } from './group.object';
|
||||
import { Torus } from './torus.object';
|
||||
import { Capsule } from './capsule.object';
|
||||
import { MeshPart } from './mesh.object';
|
||||
import { Humanoid } from './humanoid.object';
|
||||
|
||||
export const instancableGameObjects: Record<string, Instancable<GameObject>> = {
|
||||
[Group.name]: Group,
|
||||
[Brick.name]: Brick,
|
||||
[Cylinder.name]: Cylinder,
|
||||
[Sphere.name]: Sphere,
|
||||
[Torus.name]: Torus,
|
||||
[Capsule.name]: Capsule,
|
||||
[Wedge.name]: Wedge,
|
||||
[WedgeCorner.name]: WedgeCorner,
|
||||
[WedgeInnerCorner.name]: WedgeInnerCorner,
|
||||
|
@ -20,4 +26,16 @@ export const instancableGameObjects: Record<string, Instancable<GameObject>> = {
|
|||
|
||||
export * from './environment.object';
|
||||
export * from './world.object';
|
||||
export { Group, Cylinder, Brick, Sphere, Wedge, WedgeCorner, WedgeInnerCorner };
|
||||
export {
|
||||
Group,
|
||||
Cylinder,
|
||||
Brick,
|
||||
Sphere,
|
||||
Torus,
|
||||
Capsule,
|
||||
Wedge,
|
||||
WedgeCorner,
|
||||
WedgeInnerCorner,
|
||||
MeshPart,
|
||||
Humanoid,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import {
|
||||
BufferGeometry,
|
||||
Material,
|
||||
Mesh,
|
||||
MeshPhongMaterial,
|
||||
SkinnedMesh,
|
||||
} from 'three';
|
||||
import { Brick } from '.';
|
||||
|
||||
export class MeshPart extends Brick {
|
||||
public objectType = MeshPart.name;
|
||||
|
||||
constructor(
|
||||
geometry: BufferGeometry,
|
||||
material = new MeshPhongMaterial(),
|
||||
mesh: Mesh = new Mesh(geometry, material)
|
||||
) {
|
||||
super(geometry, material, mesh);
|
||||
}
|
||||
|
||||
getMesh() {
|
||||
return this.mesh;
|
||||
}
|
||||
|
||||
/** Do some surgery to convert a loaded SkinnedMesh into a game object */
|
||||
static fromLoaded(loaded: SkinnedMesh) {
|
||||
const newObject = new MeshPart(loaded.geometry, undefined, loaded);
|
||||
newObject.mesh.material = newObject.material;
|
||||
newObject.add(loaded);
|
||||
return newObject;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { Mesh } from 'three';
|
||||
import { Brick } from './brick.object';
|
||||
import { gameObjectGeometries } from './geometries';
|
||||
|
||||
export class Torus extends Brick {
|
||||
public objectType = Torus.name;
|
||||
protected mesh = new Mesh(this.geometry, this.material);
|
||||
|
||||
constructor() {
|
||||
super(gameObjectGeometries.torusGeometry);
|
||||
this.name = this.objectType;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
import { Texture } from 'three';
|
||||
import { AnimationClip, Object3D, Texture } from 'three';
|
||||
|
||||
export type AssetType = 'Texture' | 'CubeTexture' | 'Mesh';
|
||||
|
||||
export interface Asset {
|
||||
name: string;
|
||||
path?: string;
|
||||
type: 'Texture' | 'CubeTexture';
|
||||
type: AssetType;
|
||||
texture?: Texture;
|
||||
object?: Object3D;
|
||||
animations?: AnimationClip[];
|
||||
data: any;
|
||||
remote?: boolean;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@ export class GameObject extends Object3D {
|
|||
public objectType = 'GameObject';
|
||||
public virtual = false;
|
||||
|
||||
@EditorProperty({ type: String, exposed: false, readonly: true })
|
||||
public override uuid!: string;
|
||||
|
||||
@EditorProperty({ type: String })
|
||||
public override name: string = '';
|
||||
|
||||
|
@ -28,9 +31,7 @@ export class GameObject extends Object3D {
|
|||
get properties() {
|
||||
const exclude = this.excludedProperties;
|
||||
const properties = readMetadataOf<Property>(this, 'properties');
|
||||
return properties.filter(
|
||||
(item) => !exclude.includes(item.definition.name!)
|
||||
);
|
||||
return properties.filter((item) => !exclude.includes(item.name!));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,7 +47,7 @@ export class GameObject extends Object3D {
|
|||
visible: this.visible,
|
||||
};
|
||||
|
||||
const keys = this.properties.map((property) => property.definition.name!);
|
||||
const keys = this.properties.map((property) => property.name!);
|
||||
|
||||
Object.assign(
|
||||
object,
|
||||
|
@ -69,14 +70,32 @@ export class GameObject extends Object3D {
|
|||
override copy(object: Object3D, recursive = true) {
|
||||
super.copy(object as any, recursive);
|
||||
this.properties
|
||||
.map((property) => property.definition.name!)
|
||||
.filter((key) => !['position', 'rotation', 'scale'].includes(key))
|
||||
.forEach((key) => {
|
||||
(this as any)[key] = (object as any)[key];
|
||||
});
|
||||
.map((property) => property.name!)
|
||||
.filter((key) => !['position', 'rotation', 'scale', 'uuid'].includes(key))
|
||||
.forEach((key) => this.setOwnProperty(key, (object as any)[key]));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function can be used to modify any property of the object by key.
|
||||
* @param key Key to set
|
||||
* @param value Value to set
|
||||
*/
|
||||
public setOwnProperty(key: keyof GameObject, value: unknown): void;
|
||||
public setOwnProperty(key: string, value: unknown): void;
|
||||
public setOwnProperty(key: string, value: unknown) {
|
||||
const indexable = this as any;
|
||||
if (indexable[key]?.fromArray && Array.isArray(value)) {
|
||||
indexable[key].fromArray(value);
|
||||
} else if (indexable[key]?.isColor) {
|
||||
indexable[key] = new Color(value as string);
|
||||
} else if (indexable[key]?.copy) {
|
||||
indexable[key].copy(value);
|
||||
} else {
|
||||
indexable[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a serialized object into properties on this object.
|
||||
* @param input Serialized information
|
||||
|
@ -84,18 +103,7 @@ export class GameObject extends Object3D {
|
|||
deserialize(input: SerializedObject) {
|
||||
Object.keys(input)
|
||||
.filter((key) => !['children', 'objectType'].includes(key))
|
||||
.forEach((key) => {
|
||||
const indexable = this as any;
|
||||
if (indexable[key]?.fromArray && Array.isArray(input[key])) {
|
||||
indexable[key].fromArray(input[key]);
|
||||
} else if (indexable[key]?.isColor) {
|
||||
indexable[key] = new Color(input[key] as string);
|
||||
} else if (indexable[key]?.copy) {
|
||||
indexable[key].copy(input[key]);
|
||||
} else {
|
||||
indexable[key] = input[key];
|
||||
}
|
||||
});
|
||||
.forEach((key) => this.setOwnProperty(key, input[key]));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
import { Material, Object3D } from 'three';
|
||||
|
||||
export interface PropertyDefinition {
|
||||
name?: string;
|
||||
name: string;
|
||||
type?: any;
|
||||
description?: string;
|
||||
exposed?: boolean;
|
||||
readonly?: boolean;
|
||||
validators?: Function[];
|
||||
postChange?(obj: Object3D | Material): void;
|
||||
}
|
||||
|
||||
export class Property {
|
||||
public definition!: PropertyDefinition;
|
||||
export class Property implements PropertyDefinition {
|
||||
public name!: string;
|
||||
public type?: any;
|
||||
public description?: string;
|
||||
public exposed?: boolean;
|
||||
public readonly?: boolean;
|
||||
public validators?: Function[];
|
||||
public postChange?(obj: Object3D | Material): void;
|
||||
|
||||
constructor(definition: PropertyDefinition) {
|
||||
this.definition = Object.assign(
|
||||
Object.assign(
|
||||
this,
|
||||
{
|
||||
exposed: true,
|
||||
validators: [],
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export interface Ticking {
|
||||
isTickingObject: boolean;
|
||||
initialize(): void;
|
||||
tick(dt: number): void;
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import {
|
||||
AnimationClip,
|
||||
Bone,
|
||||
Object3D,
|
||||
SkeletonHelper,
|
||||
SkinnedMesh,
|
||||
} from 'three';
|
||||
import { assetManager } from '../assets';
|
||||
import { Group } from '../gameobjects/group.object';
|
||||
import { MeshPart } from '../gameobjects/mesh.object';
|
||||
import { Humanoid } from '../gameobjects/humanoid.object';
|
||||
import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
|
||||
|
||||
const cachedMeta: {
|
||||
faceTexture: string;
|
||||
root: Object3D | null;
|
||||
clips: AnimationClip[];
|
||||
} = {
|
||||
faceTexture: '',
|
||||
root: null,
|
||||
clips: [],
|
||||
};
|
||||
|
||||
export const loadBaseCharacter = async () => {
|
||||
if (cachedMeta.root) return cachedMeta;
|
||||
|
||||
// TODO: temporary loading method
|
||||
const loadMesh = await assetManager.loadMeshData(
|
||||
'https://lunasqu.ee/freeblox/character-base.glb'
|
||||
);
|
||||
const loadFace = await assetManager.createAsset(
|
||||
'https://lunasqu.ee/freeblox/face-1.png',
|
||||
'CharacterFace1',
|
||||
'Texture'
|
||||
);
|
||||
|
||||
loadFace.texture!.flipY = false;
|
||||
loadFace.texture!.needsUpdate = true;
|
||||
cachedMeta.faceTexture = loadFace.path!;
|
||||
cachedMeta.root = loadMesh.scene;
|
||||
cachedMeta.clips = loadMesh.animations;
|
||||
return cachedMeta;
|
||||
};
|
||||
|
||||
export const instanceCharacterObject = async () => {
|
||||
const base = await loadBaseCharacter();
|
||||
|
||||
const cloned = SkeletonUtils.clone(base.root!);
|
||||
|
||||
const baseObject = new Group();
|
||||
const armature = cloned.getObjectByName('Armature');
|
||||
const bone = armature!.getObjectByName('BRoot');
|
||||
const bodyParts = [
|
||||
armature!.getObjectByName('Head'),
|
||||
armature!.getObjectByName('Torso'),
|
||||
armature!.getObjectByName('ArmL'),
|
||||
armature!.getObjectByName('ArmR'),
|
||||
armature!.getObjectByName('LegR'),
|
||||
armature!.getObjectByName('LegL'),
|
||||
] as SkinnedMesh[];
|
||||
|
||||
const convertedBodyParts = bodyParts.map(MeshPart.fromLoaded);
|
||||
|
||||
const [head, torso, armL, armR, legR, legL] = convertedBodyParts;
|
||||
|
||||
head.name = 'Head';
|
||||
torso.name = 'Torso';
|
||||
armL.name = 'ArmLeft';
|
||||
armR.name = 'ArmRight';
|
||||
legR.name = 'LegRight';
|
||||
legL.name = 'LegLeft';
|
||||
|
||||
baseObject.animations = base.clips;
|
||||
baseObject.add(bone as Bone);
|
||||
convertedBodyParts.forEach((object) => baseObject.add(object));
|
||||
|
||||
head.texture = base.faceTexture;
|
||||
|
||||
const controller = new Humanoid();
|
||||
baseObject.add(controller);
|
||||
|
||||
console.log(baseObject);
|
||||
return baseObject;
|
||||
};
|
|
@ -2,3 +2,4 @@ export * from './clamp';
|
|||
export * from './debounce';
|
||||
export * from './events';
|
||||
export * from './read-metadata';
|
||||
export * from './character';
|
||||
|
|
Loading…
Reference in New Issue