Handle collision layers.

This commit is contained in:
Davy Hélard
2024-11-07 17:47:50 +01:00
parent 4230ef7780
commit acaee8e728
4 changed files with 481 additions and 46 deletions

View File

@@ -164,6 +164,16 @@ module.exports = {
return true;
}
if (propertyName === 'layers') {
behaviorContent.getChild('layers').setIntValue(parseInt(newValue, 10));
return true;
}
if (propertyName === 'masks') {
behaviorContent.getChild('masks').setIntValue(parseInt(newValue, 10));
return true;
}
return false;
};
behavior.getProperties = function (behaviorContent) {
@@ -377,6 +387,20 @@ module.exports = {
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setGroup(_('Gravity'))
.setAdvanced(true);
behaviorProperties
.getOrCreate('layers')
.setValue(behaviorContent.getChild('layers').getIntValue().toString(10))
.setType('Number')
.setLabel('Layers')
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
behaviorProperties
.getOrCreate('masks')
.setValue(behaviorContent.getChild('masks').getIntValue().toString(10))
.setType('Number')
.setLabel('Masks')
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
return behaviorProperties;
};
@@ -398,6 +422,8 @@ module.exports = {
behaviorContent.addChild('linearDamping').setDoubleValue(0.1);
behaviorContent.addChild('angularDamping').setDoubleValue(0.1);
behaviorContent.addChild('gravityScale').setDoubleValue(1);
behaviorContent.addChild('layers').setIntValue(1 << 4 | 1 << 0);
behaviorContent.addChild('masks').setIntValue(1 << 4 | 1 << 0);
};
const sharedData = new gd.BehaviorSharedDataJsImplementation();
@@ -436,7 +462,6 @@ module.exports = {
sharedContent.getChild('worldScale').setDoubleValue(newValueAsNumber);
return true;
}
return false;
};
sharedData.getProperties = function (sharedContent) {
@@ -464,7 +489,6 @@ module.exports = {
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getNewton());
sharedProperties
.getOrCreate('worldScale')
.setValue(

View File

@@ -46,46 +46,36 @@ namespace gdjs {
props: Physics3DNetworkSyncDataType;
}
// https://github.com/jrouwe/JoltPhysics.js/blob/main/Examples/js/example.js
const LAYER_NON_MOVING = 0;
const LAYER_MOVING = 1;
const NUM_OBJECT_LAYERS = 2;
// There are 4 bits for static layers and 4 bits for dynamic layers.
const staticLayersMask = 0x0f;
const dynamicLayersMask = 0xf0;
const allLayersMask = 0xff;
const setupCollisionFiltering = (settings: Jolt.JoltSettings) => {
// Layer that objects can be in, determines which other objects it can collide with
// Typically you at least want to have 1 layer for moving bodies and 1 layer for static bodies, but you can have more
// layers if you want. E.g. you could have a layer for high detail collision (which is not used by the physics simulation
// but only if you do collision testing).
let objectFilter = new Jolt.ObjectLayerPairFilterTable(NUM_OBJECT_LAYERS);
objectFilter.EnableCollision(LAYER_NON_MOVING, LAYER_MOVING);
objectFilter.EnableCollision(LAYER_MOVING, LAYER_MOVING);
// Each broadphase layer results in a separate bounding volume tree in the broad phase. You at least want to have
// a layer for non-moving and moving objects to avoid having to update a tree full of static objects every frame.
// You can have a 1-on-1 mapping between object layers and broadphase layers (like in this case) but if you have
// many object layers you'll be creating many broad phase trees, which is not efficient.
const BP_LAYER_NON_MOVING = new Jolt.BroadPhaseLayer(0);
const BP_LAYER_MOVING = new Jolt.BroadPhaseLayer(1);
const NUM_BROAD_PHASE_LAYERS = 2;
let bpInterface = new Jolt.BroadPhaseLayerInterfaceTable(
NUM_OBJECT_LAYERS,
NUM_BROAD_PHASE_LAYERS
const objectFilter = new Jolt.ObjectLayerPairFilterMask();
const staticBroadPhaseLayer = new Jolt.BroadPhaseLayer(0);
const dynamicBroadPhaseLayer = new Jolt.BroadPhaseLayer(1);
const broadPhaseLayerInterfaceMask = new Jolt.BroadPhaseLayerInterfaceMask(
2
);
bpInterface.MapObjectToBroadPhaseLayer(
LAYER_NON_MOVING,
BP_LAYER_NON_MOVING
broadPhaseLayerInterfaceMask.ConfigureLayer(
staticBroadPhaseLayer,
staticLayersMask,
0
);
bpInterface.MapObjectToBroadPhaseLayer(LAYER_MOVING, BP_LAYER_MOVING);
broadPhaseLayerInterfaceMask.ConfigureLayer(
dynamicBroadPhaseLayer,
dynamicLayersMask,
0
);
// BroadPhaseLayer have been copied into bpInterface
Jolt.destroy(staticBroadPhaseLayer);
Jolt.destroy(dynamicBroadPhaseLayer);
settings.mObjectLayerPairFilter = objectFilter;
settings.mBroadPhaseLayerInterface = bpInterface;
settings.mBroadPhaseLayerInterface = broadPhaseLayerInterfaceMask;
settings.mObjectVsBroadPhaseLayerFilter =
new Jolt.ObjectVsBroadPhaseLayerFilterTable(
settings.mBroadPhaseLayerInterface,
NUM_BROAD_PHASE_LAYERS,
settings.mObjectLayerPairFilter,
NUM_OBJECT_LAYERS
);
new Jolt.ObjectVsBroadPhaseLayerFilterMask(broadPhaseLayerInterfaceMask);
};
export class Physics3DSharedData {
@@ -144,10 +134,8 @@ namespace gdjs {
const bodyA = Jolt.wrapPointer(bodyPtrA, Jolt.Body);
const bodyB = Jolt.wrapPointer(bodyPtrB, Jolt.Body);
// Get associated behaviors
const behaviorA = bodyA.gdjsAssociatedBehavior;
const behaviorB = bodyB.gdjsAssociatedBehavior;
if (!behaviorA || !behaviorB) {
return;
}
@@ -184,14 +172,14 @@ namespace gdjs {
behaviorB.onContactEnd(behaviorA);
};
this.contactListener.OnContactPersisted = (
inBody1: number,
inBody2: number,
inManifold: number,
ioSettings: number
bodyPtrA: number,
bodyPtrB: number,
manifoldPtr: number,
settingsPtr: number
): void => {};
this.contactListener.OnContactValidate = (
inBody1: number,
inBody2: number,
bodyPtrA: number,
bodyPtrB: number,
inBaseOffset: number,
inCollisionResult: number
): number => {
@@ -298,6 +286,8 @@ namespace gdjs {
linearDamping: float;
angularDamping: float;
gravityScale: float;
layers: integer;
masks: integer;
shapeScale: number = 1;
/**
@@ -367,6 +357,8 @@ namespace gdjs {
this.linearDamping = Math.max(0, behaviorData.linearDamping);
this.angularDamping = Math.max(0, behaviorData.angularDamping);
this.gravityScale = behaviorData.gravityScale;
this.layers = behaviorData.layers;
this.masks = behaviorData.masks;
this.destroyedDuringFrameLogic = false;
this._sharedData = Physics3DSharedData.getSharedData(
instanceContainer.getScene(),
@@ -588,7 +580,14 @@ namespace gdjs {
: this.bodyType === 'Kinematic'
? Jolt.EMotionType_Kinematic
: Jolt.EMotionType_Dynamic,
LAYER_MOVING
Jolt.ObjectLayerPairFilterMask.prototype.sGetObjectLayer(
// Make sure objects don't register in the wrong layer group.
this.bodyType === 'Static'
? this.layers & staticLayersMask
: this.layers & dynamicLayersMask,
// Static objects accept all collisions as it's the mask of dynamic objects that matters.
this.bodyType === 'Static' ? allLayersMask : this.masks
)
);
bodyCreationSettings.mMotionQuality = this.bullet
? Jolt.EMotionQuality_LinearCast

View File

@@ -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.
@@ -13,8 +14,8 @@ const BehaviorsEditorService = {
return this.components[behaviorType].component; // Custom behavior editor
},
components: {
'Physics2::Physics2Behavior': {
component: Physics2Editor,
'Physics3D::Physics3DBehavior': {
component: Physics3DEditor,
},
},
};

View File

@@ -0,0 +1,411 @@
// @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="physics2-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>
<Line>
<SelectField
id="physics2-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>
</Line>
<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
properties={properties}
propertyName={'shapeOffsetX'}
step={1}
onUpdate={newValue =>
updateBehaviorProperty('shapeOffsetX', newValue)
}
/>
<NumericProperty
properties={properties}
propertyName={'shapeOffsetY'}
step={1}
onUpdate={newValue =>
updateBehaviorProperty('shapeOffsetY', newValue)
}
/>
<NumericProperty
properties={properties}
propertyName={'shapeOffsetZ'}
step={1}
onUpdate={newValue =>
updateBehaviorProperty('shapeOffsetZ', newValue)
}
/>
</ResponsiveLineStackLayout>
<ResponsiveLineStackLayout>
<NumericProperty
id="physics2-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="physics2-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;