implementing basic character

This commit is contained in:
Evert Prants 2023-06-18 00:33:55 +03:00
parent 4c44c7923f
commit d40e2073ee
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
21 changed files with 486 additions and 114 deletions

View File

@ -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>

View File

@ -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,
});
}

View File

@ -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;

View File

@ -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();

View File

@ -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);
});

View File

@ -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();

View File

@ -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) {}

View File

@ -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(

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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();
}
}

View File

@ -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,
};

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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]));
}
}

View File

@ -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: [],

View File

@ -0,0 +1,5 @@
export interface Ticking {
isTickingObject: boolean;
initialize(): void;
tick(dt: number): void;
}

View File

@ -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;
};

View File

@ -2,3 +2,4 @@ export * from './clamp';
export * from './debounce';
export * from './events';
export * from './read-metadata';
export * from './character';