From b06a1585232c4dce66d1589ffe69f12692897a12 Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Tue, 19 Sep 2023 07:23:01 -0500 Subject: [PATCH] Soccer Components. --- src/app.ts | 8 +- src/controllers/base.ts | 19 ++++- src/controllers/controllers.ts | 3 +- src/controllers/rigplatform.ts | 20 +++++ src/diagram/diagramEventHandler.ts | 2 +- src/soccer/ball.ts | 5 +- src/soccer/field.ts | 51 +++++++++-- src/soccer/player.ts | 133 +++++++++++++++++++++++------ src/soccer/team.ts | 46 ++++++++++ 9 files changed, 241 insertions(+), 46 deletions(-) diff --git a/src/app.ts b/src/app.ts index 07d1c10..1a5f531 100644 --- a/src/app.ts +++ b/src/app.ts @@ -20,6 +20,7 @@ import workerUrl from "./worker?worker&url"; import {DiagramEventType} from "./diagram/diagramEntity"; import {PeerjsNetworkConnection} from "./integration/peerjsNetworkConnection"; import {DiagramExporter} from "./util/diagramExporter"; +import {Field} from "./soccer/field"; export class App { @@ -116,6 +117,7 @@ export class App { camera.attachControl(canvas, true); new HemisphericLight("light1", new Vector3(1, 1, 0), scene); + environment.groundMeshObservable.add(async (ground) => { const xr = await WebXRDefaultExperience.CreateAsync(scene, { floorMeshes: [ground], @@ -137,8 +139,8 @@ export class App { }); }); - xr.baseExperience.onStateChangedObservable.add((state) => { + xr.baseExperience.onStateChangedObservable.add((state) => { if (state == WebXRState.IN_XR) { scene.audioEnabled = true; xr.baseExperience.camera.position = new Vector3(0, 1.6, 0); @@ -153,6 +155,10 @@ export class App { }); import('./controllers/rigplatform').then((rigmodule) => { const rig = new rigmodule.Rigplatform(scene, xr, diagramManager, controllers); + setTimeout(() => { + const field = new Field(scene); + field.addControllers(controllers); + }, 5000); }); }); diff --git a/src/controllers/base.ts b/src/controllers/base.ts index b061d79..7e5b63b 100644 --- a/src/controllers/base.ts +++ b/src/controllers/base.ts @@ -1,6 +1,7 @@ import { AbstractMesh, HavokPlugin, + Mesh, PhysicsMotionType, Scene, TransformNode, @@ -108,17 +109,27 @@ export class Base { } private grab() { - const mesh = this.xr.pointerSelection.getMeshUnderPointer(this.controller.uniqueId); + let mesh = this.xr.pointerSelection.getMeshUnderPointer(this.controller.uniqueId); if (!mesh) { return; } + let player = false; const template = mesh?.metadata?.template; if (!template) { if (mesh?.metadata?.handle == true) { mesh && mesh.setParent(this.controller.motionController.rootMesh); this.grabbedMesh = mesh; } else { - return; + if (mesh?.parent?.parent?.metadata?.grabbable) { + if (mesh?.parent?.parent?.parent) { + mesh = (mesh?.parent?.parent?.parent as Mesh); + this.grabbedMesh = mesh; + player = true; + } + } else { + return; + } + } } else { if (template == '#connection-template') { @@ -131,7 +142,7 @@ export class Base { this.previousScaling = mesh?.scaling.clone(); this.previousPosition = mesh?.position.clone(); - if ("toolbox" != mesh?.parent?.parent?.id) { + if (("toolbox" != mesh?.parent?.parent?.id) || player) { if (mesh.physicsBody) { const transformNode = setupTransformNode(mesh, this.controller.motionController.rootMesh); mesh.physicsBody.setMotionType(PhysicsMotionType.ANIMATED); @@ -196,7 +207,7 @@ export class Base { this.previousRotation = null; this.previousPosition = null; this.grabbedMesh = null; - if (mesh?.metadata?.template.indexOf('#') == -1) { + if (mesh?.metadata?.template && (mesh?.metadata?.template.indexOf('#') == -1)) { return; } const entity = toDiagramEntity(mesh); diff --git a/src/controllers/controllers.ts b/src/controllers/controllers.ts index 0261e5d..54a510a 100644 --- a/src/controllers/controllers.ts +++ b/src/controllers/controllers.ts @@ -29,7 +29,8 @@ export enum ControllerEventType { UP_DOWN = 'updown', TRIGGER = 'trigger', MENU = 'menu', - MOTION = 'motion' + MOTION = 'motion', + GAZEPOINT = 'gazepoint', } export class Controllers { diff --git a/src/controllers/rigplatform.ts b/src/controllers/rigplatform.ts index cfa7a3f..5e10d18 100644 --- a/src/controllers/rigplatform.ts +++ b/src/controllers/rigplatform.ts @@ -124,6 +124,26 @@ export class Rigplatform { case ControllerEventType.MENU: this.bMenu.toggle(); break; + case ControllerEventType.TRIGGER: + const worldRay = this.scene.activeCamera.getForwardRay(); + worldRay.origin = this.scene.activeCamera.globalPosition; + const pickInfo = this.scene.pickWithRay(worldRay, null); + if (pickInfo?.hit) { + const circle = MeshBuilder.CreateSphere("circle", {diameter: .02}, this.scene); + circle.position = pickInfo.pickedPoint; + setTimeout(() => { + circle.dispose(); + }, 500); + + if (pickInfo?.pickedMesh?.name == 'Football Ball.001') { + this.controllers.controllerObserver.notifyObservers({ + type: ControllerEventType.GAZEPOINT, + endPosition: pickInfo.pickedPoint, + startPosition: this.xr.baseExperience.camera.globalPosition + }) + } + } + break; case ControllerEventType.MOTION: console.log(JSON.stringify(event)); this.buildKickLine(event.startPosition, event.endPosition); diff --git a/src/diagram/diagramEventHandler.ts b/src/diagram/diagramEventHandler.ts index fa7184d..fa21e33 100644 --- a/src/diagram/diagramEventHandler.ts +++ b/src/diagram/diagramEventHandler.ts @@ -37,7 +37,7 @@ export function diagramEventHandler(event: DiagramEvent, case DiagramEventType.DROPPED: break; case DiagramEventType.DROP: - if (mesh.metadata.template.indexOf('#') > -1) { + if (mesh?.metadata?.template && (mesh.metadata.template.indexOf('#') > -1)) { TextLabel.updateTextNode(mesh, entity.text); } break; diff --git a/src/soccer/ball.ts b/src/soccer/ball.ts index 6083634..8dcdec0 100644 --- a/src/soccer/ball.ts +++ b/src/soccer/ball.ts @@ -52,8 +52,9 @@ export class Ball { this.scene?.getPhysicsEngine()?.getPhysicsPlugin()) { console.log("creating physics aggregate"); this.physicsAggregate = new PhysicsAggregate(this.parent, - PhysicsShapeType.SPHERE, {mass: 1, restitution: .5, friction: .6}, this.scene); - this.physicsAggregate.body.setLinearDamping(.2); + PhysicsShapeType.SPHERE, {mass: 1, restitution: .6, friction: .6}, this.scene); + this.physicsAggregate.body.setLinearDamping(.3); + this.physicsAggregate.body.setAngularDamping(2); this.mesh.setParent(this.physicsAggregate.transformNode); this.mesh.position.y = 0; return; diff --git a/src/soccer/field.ts b/src/soccer/field.ts index 8210230..b7821b8 100644 --- a/src/soccer/field.ts +++ b/src/soccer/field.ts @@ -1,31 +1,61 @@ -import {InstancedMesh, Mesh, MeshBuilder, Scene, StandardMaterial, Vector2, Vector3} from "@babylonjs/core"; +import { + InstancedMesh, + Mesh, + MeshBuilder, + Scene, + StandardMaterial, + TransformNode, + Vector2, + Vector3 +} from "@babylonjs/core"; import {Ball} from "./ball"; -import {Rigplatform} from "../controllers/rigplatform"; +import {Team} from "./team"; +import {ControllerEventType, Controllers} from "../controllers/controllers"; export class Field { private readonly scene: Scene; private ball: Ball; - private rig: Rigplatform; + private controllers: Controllers; private goalMesh: Mesh; private material: StandardMaterial; + private team1: Team; + private readonly fieldCenter: TransformNode; + private team2: Team; + private gazePoint: Vector3; constructor(scene: Scene) { this.scene = scene; + this.fieldCenter = new TransformNode("fieldCenter", this.scene); + this.team1 = new Team(scene, 1, "one"); + this.team2 = new Team(scene, -1, "two"); + this.goalMesh = MeshBuilder.CreateCylinder("goalPost", {diameter: .1, height: 1}, this.scene); this.material = new StandardMaterial("material", this.scene); this.material.diffuseColor.set(1, 1, 1); this.material.alpha = .5; this.goalMesh.material = this.getMaterial(); this.goalMesh.setEnabled(false); + this.ball = new Ball(this.scene); this.buildField(); } - public addBall(ball: Ball) { - this.ball = ball; - } + public addControllers(controllers: Controllers) { + this.controllers = controllers; + this.controllers.controllerObserver.add((event) => { + switch (event.type) { + case ControllerEventType.MOTION: + this.ball.kick(event.startPosition.clone().subtract(event.endPosition).normalize(), event.duration / 100); + break; + case ControllerEventType.GAZEPOINT: + if (event.endPosition) { + this.gazePoint = event.endPosition.clone(); + } + break; + + } + + }); - public addRig(rig: Rigplatform) { - this.rig = rig; } @@ -43,7 +73,6 @@ export class Field { this.buildLine(new Vector2(-18.33, -50 + 8.25), new Vector2(width, 16.5 - width)); this.buildLine(new Vector2(18.33, -50 + 8.25), new Vector2(width, 16.5 - width)); - this.buildLine(new Vector2(0, -50), new Vector2(70 - width, width)); this.buildLine(new Vector2(0, 50), new Vector2(70 - width, width)); @@ -83,6 +112,7 @@ export class Field { } goalPost.scaling.y = length; goalPost.visibility = 1; + goalPost.setParent(this.fieldCenter); } private buildCircle(position: Vector2) { @@ -92,6 +122,7 @@ export class Field { circle.position.y = .01; circle.rotation.x = Math.PI / 2; circle.material = this.getMaterial(); + circle.setParent(this.fieldCenter); } private buildArc(position: Vector2, arc: number = Math.PI, rotation: number = 0) { @@ -109,6 +140,7 @@ export class Field { circle.position.x = position.x; circle.position.z = position.y; circle.rotation = new Vector3(0, rotation, 0); + circle.setParent(this.fieldCenter); } private getMaterial(): StandardMaterial { @@ -125,5 +157,6 @@ export class Field { line.position.y = .01; line.position.x = position.x; line.position.z = position.y; + line.setParent(this.fieldCenter); } } \ No newline at end of file diff --git a/src/soccer/player.ts b/src/soccer/player.ts index 9bde462..a312e04 100644 --- a/src/soccer/player.ts +++ b/src/soccer/player.ts @@ -1,60 +1,137 @@ import { AbstractMesh, AnimationGroup, + AssetContainer, + Mesh, MeshBuilder, Observable, PhysicsAggregate, + PhysicsMotionType, PhysicsShapeType, Scene, SceneLoader, - TransformNode, + Skeleton, Vector2, Vector3 } from "@babylonjs/core"; +export class PlayerFactory { + public onReadyObservable: Observable = new Observable(); + private readonly scene: Scene; + private container: AssetContainer; + + constructor(scene: Scene) { + this.scene = scene; + SceneLoader.LoadAssetContainer("/assets/models/", + "player2.glb", + this.scene, + (container: AssetContainer) => { + this.container = container; + this.onReadyObservable.notifyObservers(this); + }); + + } + + public buildPlayer(position: Vector3, number: number, teamName: string = "team"): Player { + return new Player(this.scene, position, this.container, number, teamName); + } + +} + export class Player { public readonly onReadyObservable: Observable = new Observable(); private readonly scene: Scene; - private readonly position: Vector3; - private mesh: TransformNode; + private position: Vector3; + private mesh: Mesh; private parent: AbstractMesh; private animationGroup: AnimationGroup; private physicsAggregate: PhysicsAggregate; + private skeleton: Skeleton; + private number: number; + private teamName: string; + private forward = true; + private destination: Vector2; - constructor(scene: Scene, position: Vector3) { + constructor(scene: Scene, position: Vector3, container: AssetContainer, number: number = 0, teamName: string = "team") { this.scene = scene; this.position = position; + this.number = number; + this.teamName = teamName; + const data = container.instantiateModelsToScene(undefined, false, {doNotInstantiate: true}); + this.mesh = (data.rootNodes[0] as Mesh); + this.skeleton = data.skeletons[0]; + this.animationGroup = data.animationGroups[6]; this.buildPlayer(); } - buildPlayer() { - SceneLoader.ImportMesh(null, "/assets/models/", "player2.glb", this.scene, - (meshes, particleSystems, skeletons, animationGroups) => { - this.mesh = meshes[0]; - this.parent = MeshBuilder.CreateCylinder("playerParent", {diameter: .5, height: 1.6}, this.scene); - this.parent.position = this.position; - this.parent.isVisible = false; - this.physicsAggregate = new PhysicsAggregate(this.parent, - PhysicsShapeType.CYLINDER, {mass: 50, restitution: .02, friction: 0}, this.scene); - animationGroups[0].stop(); + public lookAt(location: Vector2) { + const body = this.physicsAggregate.body; - this.animationGroup = animationGroups[6]; - this.animationGroup.start(false, 1, 266, 266); - this.mesh.setParent(this.physicsAggregate.transformNode); - this.mesh.position.x = 3; - this.mesh.position.y = -.84; - this.onReadyObservable.notifyObservers(this); - }); + body.disablePreStep = false; + + body.transformNode.lookAt(new Vector3(location.x, body.transformNode.position.y, location.y)); + this.scene.onAfterRenderObservable.addOnce(() => { + + this.physicsAggregate.body.disablePreStep = true; + this.animationGroup.stop(); + this.animationGroup.onAnimationGroupEndObservable.add(() => { + if (this.forward) { + this.animationGroup.start(false, .1, 256, 267); + } else { + this.animationGroup.start(false, .1, 267, 256); + } + this.forward = !this.forward; + }, -1, false, this); + this.animationGroup.start(false, .1, 256, 267); + + }); } public runTo(location: Vector2) { - this.animationGroup.stop(); - this.animationGroup.start(true, 1.5, 0, 250); - this.physicsAggregate.body.transformNode.lookAt(new Vector3(location.x, 2, location.y)); - const speed = location.normalize().scale(2); - this.physicsAggregate.body.setLinearVelocity(new Vector3(0, 0, 3)); - //this.physicsAggregate.body.setAngularVelocity(new Vector3(0, .1, 0)); - + this.destination = location; + const body = this.physicsAggregate.transformNode.physicsBody; + body.setMotionType(PhysicsMotionType.ANIMATED); + body.disablePreStep = false; + body.transformNode.lookAt(new Vector3(location.x, body.transformNode.position.y, location.y)); + const vel = new Vector3(location.x, body.transformNode.position.y, location.y); + this.scene.onBeforeRenderObservable.addOnce(() => { + body.setLinearVelocity(vel.subtract(body.transformNode.position).normalize().scale(2)); + }); + this.scene.onAfterRenderObservable.addOnce(() => { + this.physicsAggregate.body.disablePreStep = true; + this.animationGroup.stop(); + this.animationGroup.start(true, 1.0, 0, 250); + }); + this.scene.onAfterPhysicsObservable.add(() => { + if (body.getLinearVelocity().length() > .1) { + console.log(this.destination.subtract(new Vector2(body.transformNode.position.x, + body.transformNode.position.z)).length()); + if (this.destination.subtract(new Vector2(body.transformNode.position.x, + body.transformNode.position.z)).length() < .1) { + body.setLinearVelocity(Vector3.Zero()); + body.setMotionType(PhysicsMotionType.DYNAMIC); + this.animationGroup.stop(); + this.animationGroup.start(false, .1, 256, 267); + } + } + }); } + + private buildPlayer() { + this.parent = MeshBuilder.CreateCylinder(`team${this.teamName}player${this.number}`, { + diameter: .5, + height: 1.6 + }, this.scene); + this.parent.position = this.position; + this.parent.isVisible = false; + this.physicsAggregate = new PhysicsAggregate(this.parent, + PhysicsShapeType.CYLINDER, {mass: 100, restitution: .02, friction: .3}, this.scene); + this.physicsAggregate.body.setAngularDamping(.5); + + this.mesh.parent = this.physicsAggregate.transformNode; + this.mesh.metadata = {type: "player", grabbable: true}; + this.mesh.position.x = 3; + this.mesh.position.y = -.84; + } } \ No newline at end of file diff --git a/src/soccer/team.ts b/src/soccer/team.ts index e69de29..87292f9 100644 --- a/src/soccer/team.ts +++ b/src/soccer/team.ts @@ -0,0 +1,46 @@ +import {Player, PlayerFactory} from "./player"; +import {Scene, Vector2, Vector3} from "@babylonjs/core"; + +export class Team { + private readonly scene: Scene; + private players: Player[] = []; + private goalSide: number = -1; + private playerFactory: PlayerFactory; + private positions: Vector2[] = [ + new Vector2(3, 1), + new Vector2(-3, 1), + new Vector2(5, 2), + new Vector2(15, 5), + new Vector2(-15, 5), + new Vector2(2, 10), + new Vector2(-2, 15), + new Vector2(15, 20), + new Vector2(-15, 20), + new Vector2(0, 35), + new Vector2(0, 47), + ]; + + private name: string; + + constructor(scene: Scene, side: number = 1, name: string = "team") { + this.scene = scene; + this.goalSide = side; + this.name = name; + this.playerFactory = new PlayerFactory(this.scene); + this.playerFactory.onReadyObservable.add(() => { + this.buildTeam(); + this.players[5].runTo(new Vector2(3, -3 * this.goalSide)); + }); + + } + + private buildTeam() { + for (let i = 0; i < 11; i++) { + const player = this.playerFactory + .buildPlayer(new Vector3(this.positions[i].x * this.goalSide, 1, this.positions[i].y * this.goalSide), i, + this.name); + player.lookAt(new Vector2(0, -50 * this.goalSide)) + this.players.push(player); + } + } +} \ No newline at end of file