mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Add 3D physics and 3D character behaviors (#7149)
* The 3D physics engine is powered by [Jolt Physics](https://github.com/jrouwe/JoltPhysics) a modern, performant, battle-tested, fully featured 3D physics engine used in AAA games, like Horizon: Forbidden West. The new 3D physics engine is perfect for making FPS, TPS, 3D platformer and in the future 3D racing games or any 3D game. * Similar to the 2D physics engine, you can add the 3D Physics behavior to your object. For example, add the behavior to platforms with the type set to "Static". * You can choose the collider shape: box, sphere, cylinder or capsule and modify physics properties of the objects having this behavior - much like the 2D physics engine. * For characters, a dedicated "3D Physics Character" behavior is available. Pair it with one or more additional behaviors to get controls working out of the box: "3D Shooter keyboard mapper", "3D Platformer keyboard mapper", etc... * Take a look at the new examples to give it a try! New examples and behaviors will be progressively added.
This commit is contained in:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -3,6 +3,7 @@ Extensions/ParticleSystem/SPARK/* linguist-vendored
|
||||
Extensions/PhysicsBehavior/Box2D/* linguist-vendored
|
||||
Extensions/PhysicsBehavior/box2djs/* linguist-vendored
|
||||
Extensions/Physics2Behavior/box2d.js linguist-vendored
|
||||
Extensions/Physics3DBehavior/*.wasm-compat.js linguist-vendored
|
||||
Extensions/BBText/pixi-multistyle-text/* linguist-vendored
|
||||
Extensions/P2P/A_peer.js linguist-vendored
|
||||
Extensions/Multiplayer/peer.js linguist-vendored
|
||||
|
@@ -695,7 +695,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'IsDynamic',
|
||||
_('Is dynamic'),
|
||||
_('Test if an object is dynamic.'),
|
||||
_('Check if an object is dynamic.'),
|
||||
_('_PARAM0_ is dynamic'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
@@ -727,7 +727,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'IsStatic',
|
||||
_('Is static'),
|
||||
_('Test if an object is static.'),
|
||||
_('Check if an object is static.'),
|
||||
_('_PARAM0_ is static'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
@@ -759,7 +759,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'IsKinematic',
|
||||
_('Is kinematic'),
|
||||
_('Test if an object is kinematic.'),
|
||||
_('Check if an object is kinematic.'),
|
||||
_('_PARAM0_ is kinematic'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
@@ -790,9 +790,9 @@ module.exports = {
|
||||
aut
|
||||
.addCondition(
|
||||
'IsBullet',
|
||||
_('Is treat as bullet'),
|
||||
_('Test if an object is being treat as a bullet.'),
|
||||
_('_PARAM0_ is bullet'),
|
||||
_('Is treated as a bullet'),
|
||||
_('Check if the object is being treated as a bullet.'),
|
||||
_('_PARAM0_ is treated as a bullet'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
'res/physics32.png'
|
||||
@@ -825,7 +825,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'HasFixedRotation',
|
||||
_('Has fixed rotation'),
|
||||
_('Test if an object has fixed rotation.'),
|
||||
_('Check if an object has fixed rotation.'),
|
||||
_('_PARAM0_ has fixed rotation'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
@@ -859,7 +859,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'IsSleepingAllowed',
|
||||
_('Is sleeping allowed'),
|
||||
_('Test if an object can sleep.'),
|
||||
_('Check if an object can sleep.'),
|
||||
_('_PARAM0_ can sleep'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
@@ -898,7 +898,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'IsSleeping',
|
||||
_('Is sleeping'),
|
||||
_('Test if an object is sleeping.'),
|
||||
_('Check if an object is sleeping.'),
|
||||
_('_PARAM0_ is sleeping'),
|
||||
_('Dynamics'),
|
||||
'res/physics32.png',
|
||||
@@ -1267,7 +1267,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'LayerEnabled',
|
||||
_('Layer enabled'),
|
||||
_('Test if an object has a specific layer enabled.'),
|
||||
_('Check if an object has a specific layer enabled.'),
|
||||
_('_PARAM0_ has layer _PARAM2_ enabled'),
|
||||
_('Filtering'),
|
||||
'res/physics32.png',
|
||||
@@ -1303,7 +1303,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'MaskEnabled',
|
||||
_('Mask enabled'),
|
||||
_('Test if an object has a specific mask enabled.'),
|
||||
_('Check if an object has a specific mask enabled.'),
|
||||
_('_PARAM0_ has mask _PARAM2_ enabled'),
|
||||
_('Filtering'),
|
||||
'res/physics32.png',
|
||||
@@ -1897,7 +1897,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'JointFirstObject',
|
||||
_('Joint first object'),
|
||||
_('Test if an object is the first object on a joint.'),
|
||||
_('Check if an object is the first object on a joint.'),
|
||||
_('_PARAM0_ is the first object for joint _PARAM2_'),
|
||||
_('Joints'),
|
||||
'res/physics32.png',
|
||||
@@ -1913,7 +1913,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'JointSecondObject',
|
||||
_('Joint second object'),
|
||||
_('Test if an object is the second object on a joint.'),
|
||||
_('Check if an object is the second object on a joint.'),
|
||||
_('_PARAM0_ is the second object for joint _PARAM2_'),
|
||||
_('Joints'),
|
||||
'res/physics32.png',
|
||||
@@ -2382,7 +2382,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'RevoluteJointLimitsEnabled',
|
||||
_('Revolute joint limits enabled'),
|
||||
_('Test if a revolute joint limits are enabled.'),
|
||||
_('Check if a revolute joint limits are enabled.'),
|
||||
_('Limits for revolute joint _PARAM2_ are enabled'),
|
||||
_('Joints/Revolute'),
|
||||
'JsPlatform/Extensions/revolute_joint24.png',
|
||||
@@ -2461,7 +2461,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'RevoluteJointMotorEnabled',
|
||||
_('Revolute joint motor enabled'),
|
||||
_('Test if a revolute joint motor is enabled.'),
|
||||
_('Check if a revolute joint motor is enabled.'),
|
||||
_('Motor of revolute joint _PARAM2_ is enabled'),
|
||||
_('Joints/Revolute'),
|
||||
'JsPlatform/Extensions/revolute_joint24.png',
|
||||
@@ -2700,7 +2700,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'PrismaticJointLimitsEnabled',
|
||||
_('Prismatic joint limits enabled'),
|
||||
_('Test if a prismatic joint limits are enabled.'),
|
||||
_('Check if a prismatic joint limits are enabled.'),
|
||||
_('Limits for prismatic joint _PARAM2_ are enabled'),
|
||||
_('Joints/Prismatic'),
|
||||
'JsPlatform/Extensions/prismatic_joint24.png',
|
||||
@@ -2779,7 +2779,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'PrismaticJointMotorEnabled',
|
||||
_('Prismatic joint motor enabled'),
|
||||
_('Test if a prismatic joint motor is enabled.'),
|
||||
_('Check if a prismatic joint motor is enabled.'),
|
||||
_('Motor for prismatic joint _PARAM2_ is enabled'),
|
||||
_('Joints/Prismatic'),
|
||||
'JsPlatform/Extensions/prismatic_joint24.png',
|
||||
@@ -3459,7 +3459,7 @@ module.exports = {
|
||||
.addCondition(
|
||||
'WheelJointMotorEnabled',
|
||||
_('Wheel joint motor enabled'),
|
||||
_('Test if a wheel joint motor is enabled.'),
|
||||
_('Check if a wheel joint motor is enabled.'),
|
||||
_('Motor for wheel joint _PARAM2_ is enabled'),
|
||||
_('Joints/Wheel'),
|
||||
'JsPlatform/Extensions/wheel_joint24.png',
|
||||
@@ -4203,7 +4203,7 @@ module.exports = {
|
||||
.getCodeExtraInformation()
|
||||
.addIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
|
||||
.setFunctionName('gdjs.physics2.objectsCollide');
|
||||
.setFunctionName('gdjs.physics2.areObjectsColliding');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
|
@@ -960,10 +960,10 @@ namespace gdjs {
|
||||
|
||||
updateObjectFromBody() {
|
||||
// Copy transform from body to the GD object.
|
||||
// It's possible the behavior was either deactivated or the object deleted
|
||||
// just before this doStepPreEvents (for example, another behavior deleted
|
||||
// the object during its own doStepPreEvents). If the body is null, we just
|
||||
// don't do anything (but still run the physics simulation - this is independent).
|
||||
// The body is null when the behavior was either deactivated or the object deleted.
|
||||
// It would be useless to try to recreate it as updateBodyFromObject already does it.
|
||||
// If the body is null, we just don't do anything
|
||||
// (but still run the physics simulation - this is independent).
|
||||
if (this._body !== null) {
|
||||
this.owner.setX(
|
||||
this._body.GetPosition().get_x() * this._sharedData.worldScale -
|
||||
|
@@ -1,6 +1,6 @@
|
||||
namespace gdjs {
|
||||
export namespace physics2 {
|
||||
export const objectsCollide = function (
|
||||
export const areObjectsColliding = function (
|
||||
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName: string,
|
||||
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
|
2464
Extensions/Physics3DBehavior/JsExtension.js
Normal file
2464
Extensions/Physics3DBehavior/JsExtension.js
Normal file
File diff suppressed because it is too large
Load Diff
1881
Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts
Normal file
1881
Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts
Normal file
File diff suppressed because it is too large
Load Diff
90
Extensions/Physics3DBehavior/Physics3DTools.ts
Normal file
90
Extensions/Physics3DBehavior/Physics3DTools.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
namespace gdjs {
|
||||
export namespace physics3d {
|
||||
export const areObjectsColliding = function (
|
||||
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName: string,
|
||||
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName2: string,
|
||||
inverted: boolean
|
||||
) {
|
||||
return gdjs.evtTools.object.twoListsTest(
|
||||
gdjs.Physics3DRuntimeBehavior.areObjectsColliding,
|
||||
objectsLists1,
|
||||
objectsLists2,
|
||||
inverted,
|
||||
behaviorName
|
||||
);
|
||||
};
|
||||
|
||||
export const haveObjectsStartedColliding = function (
|
||||
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName: string,
|
||||
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName2: string,
|
||||
inverted: boolean
|
||||
) {
|
||||
return gdjs.evtTools.object.twoListsTest(
|
||||
gdjs.Physics3DRuntimeBehavior.hasCollisionStartedBetween,
|
||||
objectsLists1,
|
||||
objectsLists2,
|
||||
inverted,
|
||||
behaviorName
|
||||
);
|
||||
};
|
||||
|
||||
export const haveObjectsStoppedColliding = function (
|
||||
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName: string,
|
||||
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName2: string,
|
||||
inverted: boolean
|
||||
) {
|
||||
return gdjs.evtTools.object.twoListsTest(
|
||||
gdjs.Physics3DRuntimeBehavior.hasCollisionStoppedBetween,
|
||||
objectsLists1,
|
||||
objectsLists2,
|
||||
inverted,
|
||||
behaviorName
|
||||
);
|
||||
};
|
||||
|
||||
type BehaviorNamePair = { character: string; physics: string };
|
||||
|
||||
const isOnPlatformAdapter = (
|
||||
characterObject: gdjs.RuntimeObject,
|
||||
physicsObject: gdjs.RuntimeObject,
|
||||
behaviorNamePair: BehaviorNamePair
|
||||
): boolean => {
|
||||
const characterBehavior = characterObject.getBehavior(
|
||||
behaviorNamePair.character
|
||||
) as gdjs.PhysicsCharacter3DRuntimeBehavior;
|
||||
const physicsBehavior = physicsObject.getBehavior(
|
||||
behaviorNamePair.physics
|
||||
) as gdjs.Physics3DRuntimeBehavior;
|
||||
if (!characterBehavior || !physicsBehavior) {
|
||||
return false;
|
||||
}
|
||||
return characterBehavior.isOnFloorObject(physicsBehavior);
|
||||
};
|
||||
|
||||
const behaviorNamePair: BehaviorNamePair = { character: '', physics: '' };
|
||||
|
||||
export const isOnPlatform = (
|
||||
characterObjectsLists: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
characterBehaviorName: string,
|
||||
physicsObjectsLists: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
physicsBehaviorName: string,
|
||||
inverted: boolean
|
||||
) => {
|
||||
behaviorNamePair.character = characterBehaviorName;
|
||||
behaviorNamePair.physics = physicsBehaviorName;
|
||||
return gdjs.evtTools.object.twoListsTest(
|
||||
isOnPlatformAdapter,
|
||||
characterObjectsLists,
|
||||
physicsObjectsLists,
|
||||
inverted,
|
||||
behaviorNamePair
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
1432
Extensions/Physics3DBehavior/PhysicsCharacter3DRuntimeBehavior.ts
Normal file
1432
Extensions/Physics3DBehavior/PhysicsCharacter3DRuntimeBehavior.ts
Normal file
File diff suppressed because it is too large
Load Diff
3
Extensions/Physics3DBehavior/README.md
Normal file
3
Extensions/Physics3DBehavior/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## Physics 3D Behaviors for GDevelop
|
||||
|
||||
This is the 3D physics engine for GDevelop, based on [Jolt Physics](https://github.com/jrouwe/JoltPhysics.js/) (WebAssembly, version 0.30.0).
|
4874
Extensions/Physics3DBehavior/jolt-physics.d.ts
vendored
Normal file
4874
Extensions/Physics3DBehavior/jolt-physics.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2481
Extensions/Physics3DBehavior/jolt-physics.wasm.js
Normal file
2481
Extensions/Physics3DBehavior/jolt-physics.wasm.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
Extensions/Physics3DBehavior/jolt-physics.wasm.wasm
Normal file
BIN
Extensions/Physics3DBehavior/jolt-physics.wasm.wasm
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
describe('Physics3DRuntimeBehavior', () => {});
|
@@ -231,9 +231,13 @@ namespace gdjs {
|
||||
...super.getNetworkSyncData(),
|
||||
props: {
|
||||
cs: this._currentSpeed,
|
||||
|
||||
// TODO Try to remove these 3 fields from the synch
|
||||
// They are reset every frame and are not part of the state.
|
||||
rdx: this._requestedDeltaX,
|
||||
rdy: this._requestedDeltaY,
|
||||
ldy: this._lastDeltaY,
|
||||
|
||||
cfs: this._currentFallSpeed,
|
||||
cj: this._canJump,
|
||||
ldl: this._lastDirectionIsLeft,
|
||||
|
16
README.md
16
README.md
@@ -30,17 +30,17 @@ GDevelop is a **full-featured, no-code, open-source** game development software.
|
||||
|
||||
GDevelop is composed of an **editor**, a **game engine**, an **ecosystem** of extensions as well as **online services** and commercial support.
|
||||
|
||||
| Directory | ℹ️ Description |
|
||||
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Core` | Core classes, describing the structure of a game and tools to implement the IDE and work with GDevelop games. |
|
||||
| `GDJS` | The game engine, written in TypeScript, using PixiJS and Three.js (WebGL), powering all GDevelop games. |
|
||||
| `GDevelop.js` | Bindings of `Core`, `GDJS` and `Extensions` to JavaScript (with WebAssembly), used by the IDE. |
|
||||
| `newIDE` | The game editor, written in JavaScript with React, Electron, PixiJS and Three.js. |
|
||||
| `Extensions` | Built-in extensions for the game engine, providing objects, behaviors, events and new features. All the [community extensions are on this repository](https://github.com/GDevelopApp/GDevelop-extensions). |
|
||||
| Directory | ℹ️ Description |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Core` | Core classes, describing the structure of a game and tools to implement the IDE and work with GDevelop games. |
|
||||
| `GDJS` | The game engine, written in TypeScript, using PixiJS and Three.js for 2D and 3D rendering (WebGL), powering all GDevelop games. |
|
||||
| `GDevelop.js` | Bindings of `Core`, `GDJS` and `Extensions` to JavaScript (with WebAssembly), used by the IDE. |
|
||||
| `newIDE` | The game editor, written in JavaScript with React, Electron, PixiJS and Three.js.js. |
|
||||
| `Extensions` | Built-in extensions for the game engine, providing objects, behaviors and new features. For example, this includes the physics engines running in WebAssembly (Box2D or Jolt Physics for 3D). All the [community extensions are on this repository](https://github.com/GDevelopApp/GDevelop-extensions). |
|
||||
|
||||
To learn more about GDevelop Architecture, read the [architecture overview here](Core/GDevelop-Architecture-Overview.md).
|
||||
|
||||
Pre-generated documentation of the Core library, C++ and TypeScript game engines is [available here](https://docs.gdevelop.io).
|
||||
Pre-generated documentation of the game engine is [available here](https://docs.gdevelop.io).
|
||||
|
||||
Status of the tests and builds: [](https://app.circleci.com/pipelines/github/4ian/GDevelop) [](https://gdevelop.semaphoreci.com/projects/GDevelop) [](https://ci.appveyor.com/project/4ian/gdevelop/branch/master) [](https://good-labs.github.io/greater-good-affirmation)
|
||||
|
||||
|
2
newIDE/app/public/JsPlatform/Extensions/physics3d.svg
Normal file
2
newIDE/app/public/JsPlatform/Extensions/physics3d.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14" height="14" version="1.1" viewBox="0 0 14 14" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m7 0-6 3.5v7l6 3.5 6-3.5v-7l-6-3.5z" fill="#2e388a"/><g transform="translate(-.0405 .026)" fill="none" stroke="#53a6da" stroke-width=".7"><path d="m8.294 9.112c-1.291 0.7556-2.601 1.165-3.664 1.229-1.085 0.0656-1.803-0.2277-2.095-0.7272-0.2924-0.4996-0.1966-1.269 0.3921-2.183 0.5768-0.8956 1.575-1.837 2.866-2.593 1.291-0.7556 2.601-1.165 3.664-1.229 1.085-0.06552 1.803 0.2277 2.095 0.7272s0.1964 1.269-0.3862 2.174-1.581 1.846-2.872 2.602z"/><path d="m5.787 9.112c1.291 0.7556 2.601 1.165 3.664 1.229 1.085 0.0656 1.803-0.2277 2.095-0.7272 0.2924-0.4996 0.1966-1.269-0.3921-2.183-0.5768-0.8956-1.575-1.837-2.866-2.593-1.291-0.7556-2.601-1.165-3.664-1.229-1.085-0.06552-1.803 0.2277-2.095 0.7272s-0.1964 1.269 0.3862 2.174 1.581 1.846 2.872 2.602z"/><path d="m7.038 1.75c-0.5788 0-1.194 0.4711-1.686 1.441-0.4817 0.9502-0.7898 2.287-0.7898 3.783s0.3081 2.833 0.7898 3.783c0.4916 0.9697 1.107 1.441 1.686 1.441s1.194-0.4712 1.686-1.441c0.4817-0.9502 0.7898-2.287 0.7898-3.783s-0.3081-2.833-0.7898-3.783c-0.4916-0.9698-1.107-1.441-1.686-1.441z"/></g><path d="m7 8c0.5524 0 1-0.4478 1-1 0-0.5524-0.4478-1-1-1s-1 0.4478-1 1c0 0.5524 0.4478 1 1 1z" fill="#fff" stroke-width=".6439"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14" height="14" version="1.1" viewBox="0 0 14 14" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m7 0-6 3.5v7l6 3.5 6-3.5v-7l-6-3.5z" fill="#2e388a"/><g fill="#27aae1" stroke-width=".7"><path d="m9.1 3.801v1.398h-0.6992-2.801-2.1v1.4 1.4h1.4v-1.4h0.6992v2.801h-2.1v1.4h2.1 1.4v-1.4h1.4 0.6992v1.4h1.4v-1.4-1.4h-2.1v-1.4h2.1v-1.4-1.398h-1.4z"/><path d="m7 5.2c0.7732 0 1.4-0.6268 1.4-1.4s-0.6268-1.4-1.4-1.4-1.4 0.6268-1.4 1.4 0.6268 1.4 1.4 1.4z"/></g></svg>
|
After Width: | Height: | Size: 528 B |
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import BehaviorPropertiesEditor from './Editors/BehaviorPropertiesEditor';
|
||||
import Physics2Editor from './Editors/Physics2Editor';
|
||||
import Physics3DEditor from './Editors/Physics3DEditor';
|
||||
|
||||
/**
|
||||
* A service returning editor components for each behavior type.
|
||||
@@ -16,6 +17,9 @@ const BehaviorsEditorService = {
|
||||
'Physics2::Physics2Behavior': {
|
||||
component: Physics2Editor,
|
||||
},
|
||||
'Physics3D::Physics3DBehavior': {
|
||||
component: Physics3DEditor,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
402
newIDE/app/src/BehaviorsEditor/Editors/Physics3DEditor/index.js
Normal file
402
newIDE/app/src/BehaviorsEditor/Editors/Physics3DEditor/index.js
Normal file
@@ -0,0 +1,402 @@
|
||||
// @flow
|
||||
import { t } from '@lingui/macro';
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Line, Column, Spacer } from '../../../UI/Grid';
|
||||
import Checkbox from '../../../UI/Checkbox';
|
||||
import SelectField from '../../../UI/SelectField';
|
||||
import SelectOption from '../../../UI/SelectOption';
|
||||
import SemiControlledTextField from '../../../UI/SemiControlledTextField';
|
||||
import { getMeasurementUnitShortLabel } from '../../../PropertiesEditor/PropertiesMapToSchema';
|
||||
import MeasurementUnitDocumentation from '../../../PropertiesEditor/MeasurementUnitDocumentation';
|
||||
import { type BehaviorEditorProps } from '../BehaviorEditorProps.flow';
|
||||
import Text from '../../../UI/Text';
|
||||
import DismissableAlertMessage from '../../../UI/DismissableAlertMessage';
|
||||
import { ResponsiveLineStackLayout } from '../../../UI/Layout';
|
||||
import useForceUpdate from '../../../Utils/UseForceUpdate';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import ButtonGroup from '@material-ui/core/ButtonGroup';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
|
||||
type Props = BehaviorEditorProps;
|
||||
|
||||
const NumericProperty = (props: {|
|
||||
id?: string,
|
||||
properties: gdMapStringPropertyDescriptor,
|
||||
propertyName: string,
|
||||
step: number,
|
||||
onUpdate: (newValue: string) => void,
|
||||
|}) => {
|
||||
const { properties, propertyName, step, onUpdate, id } = props;
|
||||
const property = properties.get(propertyName);
|
||||
|
||||
return (
|
||||
<SemiControlledTextField
|
||||
id={id}
|
||||
fullWidth
|
||||
value={property.getValue()}
|
||||
key={propertyName}
|
||||
floatingLabelText={property.getLabel()}
|
||||
step={step}
|
||||
onChange={onUpdate}
|
||||
type="number"
|
||||
endAdornment={<UnitAdornment property={property} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const UnitAdornment = (props: {| property: gdPropertyDescriptor |}) => {
|
||||
const { property } = props;
|
||||
const measurementUnit = property.getMeasurementUnit();
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<MeasurementUnitDocumentation
|
||||
label={measurementUnit.getLabel()}
|
||||
description={measurementUnit.getDescription()}
|
||||
elementsWithWords={measurementUnit.getElementsWithWords()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InputAdornment position="end">
|
||||
{getMeasurementUnitShortLabel(measurementUnit)}
|
||||
</InputAdornment>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BitGroupEditor = (props: {|
|
||||
bits: Array<boolean>,
|
||||
onChange: (index: number, value: boolean) => void,
|
||||
firstIndex: number,
|
||||
disabled: boolean,
|
||||
|}) => {
|
||||
return (
|
||||
<div style={{ overflowX: 'auto', flex: 1 }}>
|
||||
<ButtonGroup disableElevation fullWidth disabled={props.disabled}>
|
||||
{props.bits.map((bit, index) => (
|
||||
<Button
|
||||
key={props.firstIndex + index}
|
||||
variant={bit ? 'contained' : 'outlined'}
|
||||
color={bit ? 'primary' : 'default'}
|
||||
onClick={() => props.onChange(props.firstIndex + index, !bit)}
|
||||
>
|
||||
{props.firstIndex + index + 1}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isBitEnabled = (bitsValue: number, pos: number) => {
|
||||
return !!(bitsValue & (1 << pos));
|
||||
};
|
||||
|
||||
const enableBit = (bitsValue: number, pos: number, enable: boolean) => {
|
||||
if (enable) bitsValue |= 1 << pos;
|
||||
else bitsValue &= ~(1 << pos);
|
||||
return bitsValue;
|
||||
};
|
||||
|
||||
const Physics3DEditor = (props: Props) => {
|
||||
const { behavior, onBehaviorUpdated } = props;
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const updateBehaviorProperty = React.useCallback(
|
||||
(property, value) => {
|
||||
behavior.updateProperty(property, value);
|
||||
forceUpdate();
|
||||
onBehaviorUpdated();
|
||||
},
|
||||
[behavior, forceUpdate, onBehaviorUpdated]
|
||||
);
|
||||
|
||||
const properties = behavior.getProperties();
|
||||
const staticBits = Array(4).fill(null);
|
||||
const dynamicBits = Array(4).fill(null);
|
||||
const shape = properties.get('shape').getValue();
|
||||
const layersValues = parseInt(properties.get('layers').getValue(), 10);
|
||||
const masksValues = parseInt(properties.get('masks').getValue(), 10);
|
||||
|
||||
const isStatic = properties.get('bodyType').getValue() === 'Static';
|
||||
|
||||
return (
|
||||
<Column
|
||||
expand
|
||||
// Avoid overflow on small screens
|
||||
noOverflowParent
|
||||
>
|
||||
<Line>
|
||||
<SelectField
|
||||
id="physics3d-parameter-body-type"
|
||||
key={'bodyType'}
|
||||
fullWidth
|
||||
floatingLabelText={properties.get('bodyType').getLabel()}
|
||||
value={properties.get('bodyType').getValue()}
|
||||
onChange={(e, i, newValue: string) =>
|
||||
updateBehaviorProperty('bodyType', newValue)
|
||||
}
|
||||
>
|
||||
{[
|
||||
<SelectOption
|
||||
key={'dynamic'}
|
||||
value={'Dynamic'}
|
||||
label={t`Dynamic`}
|
||||
/>,
|
||||
<SelectOption key={'static'} value={'Static'} label={t`Static`} />,
|
||||
<SelectOption
|
||||
key={'kinematic'}
|
||||
value={'Kinematic'}
|
||||
label={t`Kinematic`}
|
||||
/>,
|
||||
]}
|
||||
</SelectField>
|
||||
</Line>
|
||||
<ResponsiveLineStackLayout>
|
||||
<Checkbox
|
||||
label={properties.get('bullet').getLabel()}
|
||||
checked={properties.get('bullet').getValue() === 'true'}
|
||||
onCheck={(e, checked) =>
|
||||
updateBehaviorProperty('bullet', checked ? '1' : '0')
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label={properties.get('fixedRotation').getLabel()}
|
||||
checked={properties.get('fixedRotation').getValue() === 'true'}
|
||||
onCheck={(e, checked) =>
|
||||
updateBehaviorProperty('fixedRotation', checked ? '1' : '0')
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<Line>
|
||||
<DismissableAlertMessage
|
||||
identifier="physics2-shape-collisions"
|
||||
kind="info"
|
||||
>
|
||||
<Trans>
|
||||
The shape used in the Physics behavior is independent from the
|
||||
collision mask of the object. Be sure to use the "Collision"
|
||||
condition provided by the Physics behavior in the events. The usual
|
||||
"Collision" condition won't take into account the shape that you've
|
||||
set up here.
|
||||
</Trans>
|
||||
</DismissableAlertMessage>
|
||||
</Line>
|
||||
<ResponsiveLineStackLayout>
|
||||
<SelectField
|
||||
id="physics3d-parameter-shape"
|
||||
fullWidth
|
||||
floatingLabelText={properties.get('shape').getLabel()}
|
||||
value={properties.get('shape').getValue()}
|
||||
onChange={(e, i, newValue: string) =>
|
||||
updateBehaviorProperty('shape', newValue)
|
||||
}
|
||||
>
|
||||
<SelectOption key={'sphere'} value={'Sphere'} label={t`Sphere`} />
|
||||
<SelectOption key={'box'} value={'Box'} label={t`Box`} />
|
||||
<SelectOption key={'capsule'} value={'Capsule'} label={t`Capsule`} />
|
||||
<SelectOption
|
||||
key={'cylinder'}
|
||||
value={'Cylinder'}
|
||||
label={t`Cylinder`}
|
||||
/>
|
||||
</SelectField>
|
||||
<SelectField
|
||||
id="physics3d-parameter-shape-orientation"
|
||||
fullWidth
|
||||
floatingLabelText={properties.get('shapeOrientation').getLabel()}
|
||||
value={properties.get('shapeOrientation').getValue()}
|
||||
onChange={(e, i, newValue: string) =>
|
||||
updateBehaviorProperty('shapeOrientation', newValue)
|
||||
}
|
||||
disabled={
|
||||
properties.get('shape').getValue() === 'Sphere' ||
|
||||
properties.get('shape').getValue() === 'Box'
|
||||
}
|
||||
>
|
||||
<SelectOption key={'shape-orientation-z'} value={'Z'} label={t`Z`} />
|
||||
<SelectOption key={'shape-orientation-y'} value={'Y'} label={t`Y`} />
|
||||
<SelectOption key={'shape-orientation-x'} value={'X'} label={t`X`} />
|
||||
</SelectField>
|
||||
</ResponsiveLineStackLayout>
|
||||
<ResponsiveLineStackLayout>
|
||||
<SemiControlledTextField
|
||||
fullWidth
|
||||
value={properties.get('shapeDimensionA').getValue()}
|
||||
key={'shapeDimensionA'}
|
||||
floatingLabelText={shape === 'Box' ? 'Width' : 'Radius'}
|
||||
min={0}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('shapeDimensionA', newValue)
|
||||
}
|
||||
type="number"
|
||||
endAdornment={
|
||||
<UnitAdornment property={properties.get('shapeDimensionA')} />
|
||||
}
|
||||
/>
|
||||
{shape !== 'Sphere' && (
|
||||
<SemiControlledTextField
|
||||
fullWidth
|
||||
value={properties.get('shapeDimensionB').getValue()}
|
||||
key={'shapeDimensionB'}
|
||||
floatingLabelText={shape === 'Box' ? 'Width' : 'Depth'}
|
||||
min={0}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('shapeDimensionB', newValue)
|
||||
}
|
||||
type="number"
|
||||
endAdornment={
|
||||
<UnitAdornment property={properties.get('shapeDimensionB')} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{shape === 'Box' && (
|
||||
<SemiControlledTextField
|
||||
fullWidth
|
||||
value={properties.get('shapeDimensionC').getValue()}
|
||||
key={'shapeDimensionC'}
|
||||
floatingLabelText={'Depth'}
|
||||
min={0}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('shapeDimensionC', newValue)
|
||||
}
|
||||
type="number"
|
||||
endAdornment={
|
||||
<UnitAdornment property={properties.get('shapeDimensionC')} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
<ResponsiveLineStackLayout>
|
||||
<NumericProperty
|
||||
id="physics3d-parameter-density"
|
||||
properties={properties}
|
||||
propertyName={'density'}
|
||||
step={0.1}
|
||||
onUpdate={newValue =>
|
||||
updateBehaviorProperty(
|
||||
'density',
|
||||
parseFloat(newValue) > 0 ? newValue : '0'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<NumericProperty
|
||||
properties={properties}
|
||||
propertyName={'gravityScale'}
|
||||
step={0.1}
|
||||
onUpdate={newValue =>
|
||||
updateBehaviorProperty('gravityScale', newValue)
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<ResponsiveLineStackLayout>
|
||||
<NumericProperty
|
||||
properties={properties}
|
||||
propertyName={'friction'}
|
||||
step={0.1}
|
||||
onUpdate={newValue =>
|
||||
updateBehaviorProperty(
|
||||
'friction',
|
||||
parseFloat(newValue) > 0 ? newValue : '0'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<NumericProperty
|
||||
properties={properties}
|
||||
propertyName={'restitution'}
|
||||
step={0.1}
|
||||
onUpdate={newValue =>
|
||||
updateBehaviorProperty(
|
||||
'restitution',
|
||||
parseFloat(newValue) > 0 ? newValue : '0'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<ResponsiveLineStackLayout>
|
||||
<NumericProperty
|
||||
properties={properties}
|
||||
propertyName={'linearDamping'}
|
||||
step={0.05}
|
||||
onUpdate={newValue =>
|
||||
updateBehaviorProperty('linearDamping', newValue)
|
||||
}
|
||||
/>
|
||||
<NumericProperty
|
||||
id="physics3d-parameter-angular-damping"
|
||||
properties={properties}
|
||||
propertyName={'angularDamping'}
|
||||
step={0.05}
|
||||
onUpdate={newValue =>
|
||||
updateBehaviorProperty('angularDamping', newValue)
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<Line>
|
||||
<Text style={{ marginRight: 10 }}>
|
||||
{properties.get('layers').getLabel()}
|
||||
</Text>
|
||||
<BitGroupEditor
|
||||
key={'static-layers'}
|
||||
firstIndex={0}
|
||||
bits={staticBits.map(
|
||||
(_, index) => isBitEnabled(layersValues, index) && isStatic
|
||||
)}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(layersValues, index, value);
|
||||
updateBehaviorProperty('layers', newValue.toString(10));
|
||||
}}
|
||||
disabled={!isStatic}
|
||||
/>
|
||||
<Spacer />
|
||||
<BitGroupEditor
|
||||
key={'dynamic-layers'}
|
||||
firstIndex={4}
|
||||
bits={dynamicBits.map(
|
||||
(_, index) => isBitEnabled(layersValues, index + 4) && !isStatic
|
||||
)}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(layersValues, index, value);
|
||||
updateBehaviorProperty('layers', newValue.toString(10));
|
||||
}}
|
||||
disabled={isStatic}
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<Text style={{ marginRight: 10 }}>
|
||||
{properties.get('masks').getLabel()}
|
||||
</Text>
|
||||
<BitGroupEditor
|
||||
key={'static-mask'}
|
||||
firstIndex={0}
|
||||
bits={staticBits.map(
|
||||
(_, index) => isBitEnabled(masksValues, index) || isStatic
|
||||
)}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(masksValues, index, value);
|
||||
updateBehaviorProperty('masks', newValue.toString(10));
|
||||
}}
|
||||
disabled={isStatic}
|
||||
/>
|
||||
<Spacer />
|
||||
<BitGroupEditor
|
||||
key={'dynamic-mask'}
|
||||
firstIndex={4}
|
||||
bits={dynamicBits.map(
|
||||
(_, index) => isBitEnabled(masksValues, index + 4) || isStatic
|
||||
)}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(masksValues, index, value);
|
||||
updateBehaviorProperty('masks', newValue.toString(10));
|
||||
}}
|
||||
disabled={isStatic}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default Physics3DEditor;
|
Reference in New Issue
Block a user