@woosh/meep-engine 2.138.17 → 2.138.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/build/bundle-worker-image-decoder.js +1 -1
- package/editor/actions/concrete/ActionUpdateTexture.d.ts +12 -0
- package/editor/actions/concrete/ActionUpdateTexture.d.ts.map +1 -0
- package/editor/actions/concrete/ArrayCopyAction.d.ts +20 -0
- package/editor/actions/concrete/ArrayCopyAction.d.ts.map +1 -0
- package/editor/actions/concrete/ComponentRemoveAction.d.ts +11 -0
- package/editor/actions/concrete/ComponentRemoveAction.d.ts.map +1 -0
- package/editor/actions/concrete/ModifyPatchSampler2DAction.d.ts +47 -0
- package/editor/actions/concrete/ModifyPatchSampler2DAction.d.ts.map +1 -0
- package/editor/actions/concrete/ModifyPatchTextureArray2DAction.d.ts +38 -0
- package/editor/actions/concrete/ModifyPatchTextureArray2DAction.d.ts.map +1 -0
- package/editor/actions/concrete/PaintTerrainOverlayAction.d.ts +23 -0
- package/editor/actions/concrete/PaintTerrainOverlayAction.d.ts.map +1 -0
- package/editor/actions/concrete/PatchTerrainHeightAction.d.ts +19 -0
- package/editor/actions/concrete/PatchTerrainHeightAction.d.ts.map +1 -0
- package/editor/actions/concrete/SelectionRemoveAction.d.ts +10 -0
- package/editor/actions/concrete/SelectionRemoveAction.d.ts.map +1 -0
- package/editor/actions/concrete/WriteGridValueAction.d.ts +15 -0
- package/editor/actions/concrete/WriteGridValueAction.d.ts.map +1 -0
- package/editor/ecs/component/FieldDescriptor.d.ts +27 -0
- package/editor/ecs/component/FieldDescriptor.d.ts.map +1 -0
- package/editor/ecs/component/FieldValueAdapter.d.ts +7 -0
- package/editor/ecs/component/FieldValueAdapter.d.ts.map +1 -0
- package/editor/ecs/component/createFieldEditor.d.ts +9 -0
- package/editor/ecs/component/createFieldEditor.d.ts.map +1 -0
- package/editor/ecs/component/createObjectEditor.d.ts +14 -0
- package/editor/ecs/component/createObjectEditor.d.ts.map +1 -0
- package/editor/ecs/component/editors/geom/QuaternionEditor.d.ts.map +1 -1
- package/editor/ecs/component/findNearestRegisteredType.d.ts +8 -0
- package/editor/ecs/component/findNearestRegisteredType.d.ts.map +1 -0
- package/editor/process/ObstacleGridDisplayProcess.d.ts.map +1 -1
- package/editor/process/symbolic/makeGridPositionSymbolDisplay.d.ts.map +1 -1
- package/editor/tools/GridPaintTool.d.ts +17 -0
- package/editor/tools/GridPaintTool.d.ts.map +1 -0
- package/editor/tools/SelectionTool.d.ts +27 -0
- package/editor/tools/SelectionTool.d.ts.map +1 -0
- package/editor/tools/TopDownCameraControlTool.d.ts +13 -0
- package/editor/tools/TopDownCameraControlTool.d.ts.map +1 -0
- package/editor/view/ecs/ComponentControlFactory.d.ts.map +1 -0
- package/editor/view/ecs/ComponentControlView.d.ts.map +1 -1
- package/editor/view/ecs/EntityEditor.d.ts.map +1 -0
- package/editor/view/ecs/EntityList.d.ts.map +1 -0
- package/editor/view/ecs/HierarchicalEntityListView.d.ts.map +1 -0
- package/editor/view/node-graph/NodeGraphEditorView.d.ts.map +1 -1
- package/editor/view/node-graph/NodeGraphView.d.ts.map +1 -1
- package/editor/view/node-graph/NodeView.d.ts.map +1 -1
- package/editor/view/node-graph/PortView.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/binary/BinaryBuffer.d.ts +1 -1
- package/src/core/binary/BinaryBuffer.js +1 -1
- package/src/core/binary/BitSet.d.ts +1 -1
- package/src/core/binary/BitSet.js +1 -1
- package/src/core/cache/Cache.js +1 -1
- package/src/core/cache/LoadingCache.js +1 -1
- package/src/core/collection/list/List.js +1 -1
- package/src/core/collection/map/HashMap.js +1 -1
- package/src/core/collection/table/RowFirstTable.d.ts +1 -1
- package/src/core/collection/table/RowFirstTable.js +1 -1
- package/src/core/collection/table/RowFirstTableSpec.d.ts +1 -1
- package/src/core/collection/table/RowFirstTableSpec.js +1 -1
- package/src/core/color/oklab/compute_max_saturation.d.ts +1 -1
- package/src/core/color/oklab/compute_max_saturation.js +1 -1
- package/src/core/color/oklab/find_cusp.d.ts +1 -1
- package/src/core/color/oklab/find_cusp.js +1 -1
- package/src/core/color/oklab/find_gamut_intersection.d.ts +1 -1
- package/src/core/color/oklab/find_gamut_intersection.js +1 -1
- package/src/core/color/oklab/okhsv_to_linear_srgb.d.ts +1 -1
- package/src/core/color/oklab/okhsv_to_linear_srgb.js +1 -1
- package/src/core/color/oklab/oklab_to_linear_srgb.d.ts +1 -1
- package/src/core/color/oklab/oklab_to_linear_srgb.js +1 -1
- package/src/core/color/oklab/oklab_to_xyz.d.ts +1 -1
- package/src/core/color/oklab/oklab_to_xyz.js +1 -1
- package/src/core/color/oklab/xyz_to_oklab.d.ts +1 -1
- package/src/core/color/oklab/xyz_to_oklab.js +1 -1
- package/src/core/geom/2d/convex-hull/fixed_convex_hull_relaxation.d.ts +1 -1
- package/src/core/geom/2d/convex-hull/fixed_convex_hull_relaxation.js +1 -1
- package/src/core/geom/3d/aabb/aabb3_nearest_point_on_surface.d.ts.map +1 -1
- package/src/core/geom/3d/aabb/aabb3_nearest_point_on_surface.js +57 -65
- package/src/core/geom/3d/octahedra/octahedral_uv_wrap.d.ts +1 -1
- package/src/core/geom/3d/octahedra/octahedral_uv_wrap.js +1 -1
- package/src/core/geom/3d/quaternion/quat_decode_from_uint32.d.ts +1 -1
- package/src/core/geom/3d/quaternion/quat_decode_from_uint32.js +1 -1
- package/src/core/geom/3d/quaternion/quat_encode_to_uint32.d.ts +1 -1
- package/src/core/geom/3d/quaternion/quat_encode_to_uint32.js +1 -1
- package/src/core/geom/3d/shape/AbstractShape3D.d.ts +74 -3
- package/src/core/geom/3d/shape/CapsuleShape3D.d.ts +37 -0
- package/src/core/geom/3d/shape/CapsuleShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/CapsuleShape3D.js +210 -0
- package/src/core/geom/3d/shape/PointShape3D.d.ts +5 -0
- package/src/core/geom/3d/shape/PointShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/PointShape3D.js +52 -14
- package/src/core/geom/3d/shape/TransformedShape3D.d.ts +75 -12
- package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/TransformedShape3D.js +12 -3
- package/src/core/geom/3d/shape/UnionShape3D.d.ts +47 -5
- package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/UnitCubeShape3D.d.ts +12 -5
- package/src/core/geom/3d/shape/UnitCubeShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts +17 -5
- package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/shape_to_type.js +4 -1
- package/src/core/geom/3d/shape/json/type_adapters.d.ts +17 -2
- package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/type_adapters.js +18 -2
- package/src/core/geom/3d/shape/util/compute_signed_distance_gradient_by_sampling.d.ts.map +1 -1
- package/src/core/geom/3d/shape/util/compute_signed_distance_gradient_by_sampling.js +51 -48
- package/src/core/geom/3d/tetrahedra/compute_bounding_simplex_3d.d.ts +1 -1
- package/src/core/geom/3d/tetrahedra/compute_bounding_simplex_3d.js +1 -1
- package/src/core/geom/ConicRay.d.ts +1 -1
- package/src/core/geom/ConicRay.js +1 -1
- package/src/core/geom/Quaternion.d.ts +2 -2
- package/src/core/geom/Quaternion.js +2 -2
- package/src/core/geom/Vector1.d.ts +1 -1
- package/src/core/geom/Vector1.js +1 -1
- package/src/core/geom/Vector2.d.ts +1 -1
- package/src/core/geom/Vector2.js +1 -1
- package/src/core/geom/Vector3.d.ts +1 -1
- package/src/core/geom/Vector3.js +1 -1
- package/src/core/geom/packing/miniball/Miniball.d.ts +1 -1
- package/src/core/geom/packing/miniball/Miniball.js +1 -1
- package/src/core/geom/vec3/serialization/v3_binary_equality_decode.d.ts +1 -1
- package/src/core/geom/vec3/serialization/v3_binary_equality_decode.js +1 -1
- package/src/core/geom/vec3/serialization/v3_binary_equality_encode.d.ts +1 -1
- package/src/core/geom/vec3/serialization/v3_binary_equality_encode.js +1 -1
- package/src/core/math/spline/spline3_hermite.d.ts +1 -1
- package/src/core/math/spline/spline3_hermite.js +1 -1
- package/src/core/math/spline/spline3_hermite_bounds.d.ts +1 -1
- package/src/core/math/spline/spline3_hermite_bounds.js +1 -1
- package/src/core/math/spline/spline3_hermite_bounds_t.d.ts +1 -1
- package/src/core/math/spline/spline3_hermite_bounds_t.js +1 -1
- package/src/core/math/spline/spline3_hermite_to_monomial.d.ts +1 -1
- package/src/core/math/spline/spline3_hermite_to_monomial.js +1 -1
- package/src/core/model/ObservedBoolean.d.ts +1 -1
- package/src/core/model/ObservedBoolean.js +1 -1
- package/src/core/model/ObservedString.d.ts +1 -1
- package/src/core/model/ObservedString.js +1 -1
- package/src/core/process/undo/Action.js +1 -1
- package/src/core/process/undo/ActionProcessor.js +1 -1
- package/src/engine/animation/curve/AnimationCurve.d.ts +1 -1
- package/src/engine/animation/curve/AnimationCurve.js +1 -1
- package/src/engine/animation/curve/Keyframe.d.ts +1 -1
- package/src/engine/animation/curve/Keyframe.js +1 -1
- package/src/engine/animation/curve/animation_curve_compute_aabb.d.ts +1 -1
- package/src/engine/animation/curve/animation_curve_compute_aabb.js +1 -1
- package/src/engine/animation/curve/animation_curve_optimize.d.ts +1 -1
- package/src/engine/animation/curve/animation_curve_optimize.js +2 -2
- package/src/engine/asset/loaders/image/jpeg/JpegImage.js +1 -1
- package/src/engine/asset/loaders/image/png/PNGReader.d.ts.map +1 -1
- package/src/engine/asset/loaders/image/png/PNGReader.js +27 -7
- package/src/engine/asset/loaders/image/png/crc32.d.ts +1 -1
- package/src/engine/asset/loaders/image/png/crc32.js +1 -1
- package/src/engine/control/first-person/DESIGN.md +616 -0
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +229 -0
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -0
- package/src/engine/control/first-person/FirstPersonPlayerController.js +176 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +251 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +205 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +151 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +799 -0
- package/src/engine/control/first-person/math/computeJumpFromApex.d.ts +23 -0
- package/src/engine/control/first-person/math/computeJumpFromApex.d.ts.map +1 -0
- package/src/engine/control/first-person/math/computeJumpFromApex.js +23 -0
- package/src/engine/control/first-person/math/criticallyDampedSpring.d.ts +23 -0
- package/src/engine/control/first-person/math/criticallyDampedSpring.d.ts.map +1 -0
- package/src/engine/control/first-person/math/criticallyDampedSpring.js +34 -0
- package/src/engine/control/first-person/math/dampedSpringStep.d.ts +23 -0
- package/src/engine/control/first-person/math/dampedSpringStep.d.ts.map +1 -0
- package/src/engine/control/first-person/math/dampedSpringStep.js +72 -0
- package/src/engine/control/first-person/math/stepTowards.d.ts +12 -0
- package/src/engine/control/first-person/math/stepTowards.d.ts.map +1 -0
- package/src/engine/control/first-person/math/stepTowards.js +20 -0
- package/src/engine/control/first-person/pose/FirstPersonPose.d.ts +69 -0
- package/src/engine/control/first-person/pose/FirstPersonPose.d.ts.map +1 -0
- package/src/engine/control/first-person/pose/FirstPersonPose.js +91 -0
- package/src/engine/control/first-person/prototype_first_person_controller.d.ts +2 -0
- package/src/engine/control/first-person/prototype_first_person_controller.d.ts.map +1 -0
- package/src/engine/control/first-person/prototype_first_person_controller.js +314 -0
- package/src/engine/ecs/Entity.js +1 -1
- package/src/engine/ecs/EntityComponentDataset.js +1 -1
- package/src/engine/ecs/EntityManager.d.ts +1 -1
- package/src/engine/ecs/EntityManager.js +1 -1
- package/src/engine/ecs/EntityObserver.js +1 -1
- package/src/engine/ecs/EntityReference.d.ts +1 -1
- package/src/engine/ecs/EntityReference.js +1 -1
- package/src/engine/ecs/System.js +1 -1
- package/src/engine/ecs/attachment/AttachmentSystem.d.ts +2 -2
- package/src/engine/ecs/attachment/AttachmentSystem.d.ts.map +1 -1
- package/src/engine/ecs/grid/HeightMap2AOMap.d.ts.map +1 -1
- package/src/engine/ecs/grid/HeightMap2AOMap.js +3 -2
- package/src/engine/ecs/guid/UUID.d.ts +1 -1
- package/src/engine/ecs/guid/UUID.js +1 -1
- package/src/engine/ecs/name/Name.d.ts +1 -1
- package/src/engine/ecs/name/Name.js +1 -1
- package/src/engine/ecs/parent/ParentEntity.js +1 -1
- package/src/engine/ecs/storage/BinaryBufferDeSerializer.d.ts +1 -1
- package/src/engine/ecs/storage/BinaryBufferDeSerializer.js +1 -1
- package/src/engine/ecs/storage/BinaryBufferSerializer.d.ts +1 -1
- package/src/engine/ecs/storage/BinaryBufferSerializer.js +1 -1
- package/src/engine/ecs/storage/binary/BinaryClassSerializationAdapter.js +1 -1
- package/src/engine/ecs/storage/binary/BinarySerializationRegistry.d.ts +1 -1
- package/src/engine/ecs/storage/binary/BinarySerializationRegistry.js +1 -1
- package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.d.ts +1 -1
- package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.js +1 -1
- package/src/engine/ecs/transform/Transform.d.ts +1 -1
- package/src/engine/ecs/transform/Transform.js +1 -1
- package/src/engine/graphics/ecs/path/PathDisplaySystem.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/ImpostorBaker.d.ts.map +1 -1
- package/src/engine/graphics/impostors/octahedral/ImpostorBaker.js +10 -1
- package/src/engine/graphics/impostors/voxel/README.md +149 -0
- package/src/engine/graphics/impostors/voxel/VoxelImpostorBaker.d.ts +91 -0
- package/src/engine/graphics/impostors/voxel/VoxelImpostorBaker.d.ts.map +1 -0
- package/src/engine/graphics/impostors/voxel/VoxelImpostorBaker.js +376 -0
- package/src/engine/graphics/impostors/voxel/VoxelImpostorDescription.d.ts +128 -0
- package/src/engine/graphics/impostors/voxel/VoxelImpostorDescription.d.ts.map +1 -0
- package/src/engine/graphics/impostors/voxel/VoxelImpostorDescription.js +141 -0
- package/src/engine/graphics/impostors/voxel/bake/read_atlas_to_samples.d.ts +38 -0
- package/src/engine/graphics/impostors/voxel/bake/read_atlas_to_samples.d.ts.map +1 -0
- package/src/engine/graphics/impostors/voxel/bake/read_atlas_to_samples.js +338 -0
- package/src/engine/graphics/impostors/voxel/bake/voxelize_samples.d.ts +43 -0
- package/src/engine/graphics/impostors/voxel/bake/voxelize_samples.d.ts.map +1 -0
- package/src/engine/graphics/impostors/voxel/bake/voxelize_samples.js +192 -0
- package/src/engine/graphics/impostors/voxel/prototype_voxel_impostors.d.ts +2 -0
- package/src/engine/graphics/impostors/voxel/prototype_voxel_impostors.d.ts.map +1 -0
- package/src/engine/graphics/impostors/voxel/prototype_voxel_impostors.js +343 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderDepthV0.d.ts +13 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderDepthV0.d.ts.map +1 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderDepthV0.js +99 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderLitV0.d.ts +19 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderLitV0.d.ts.map +1 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderLitV0.js +231 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderNormalsV0.d.ts +5 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderNormalsV0.d.ts.map +1 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderNormalsV0.js +70 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderV0.d.ts +13 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderV0.d.ts.map +1 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderV0.js +127 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderViewportDepthV0.d.ts +5 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderViewportDepthV0.d.ts.map +1 -0
- package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderViewportDepthV0.js +68 -0
- package/src/engine/graphics/impostors/voxel/util/make_voxel_impostor_geometry.d.ts +27 -0
- package/src/engine/graphics/impostors/voxel/util/make_voxel_impostor_geometry.d.ts.map +1 -0
- package/src/engine/graphics/impostors/voxel/util/make_voxel_impostor_geometry.js +96 -0
- package/src/engine/graphics/particles/particular/engine/utils/volume/ParticleVolume.d.ts +113 -29
- package/src/engine/graphics/render/forward_plus/SPECIFICATION.md +1 -1
- package/src/engine/graphics/shaders/AmbientOcclusionShader.d.ts.map +1 -1
- package/src/engine/graphics/shaders/AmbientOcclusionShader.js +20 -5
- package/src/engine/graphics/texture/sampler/Sampler2D.d.ts +1 -1
- package/src/engine/graphics/texture/sampler/Sampler2D.js +1 -1
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_NaiveFlood.d.ts +1 -1
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_NaiveFlood.js +1 -1
- package/src/engine/graphics/util/build_max_height_pyramid.d.ts +21 -14
- package/src/engine/graphics/util/build_max_height_pyramid.d.ts.map +1 -1
- package/src/engine/graphics/util/build_max_height_pyramid.js +107 -70
- package/src/engine/input/devices/InputDeviceSwitch.js +1 -1
- package/src/engine/input/devices/KeyboardDevice.js +1 -1
- package/src/engine/input/devices/PointerDevice.js +1 -1
- package/src/engine/intelligence/behavior/Behavior.js +1 -1
- package/src/engine/intelligence/behavior/SelectorBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/SelectorBehavior.js +1 -1
- package/src/engine/intelligence/behavior/behavior_to_dot.d.ts +1 -1
- package/src/engine/intelligence/behavior/behavior_to_dot.js +1 -1
- package/src/engine/intelligence/behavior/composite/CompositeBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/composite/CompositeBehavior.js +1 -1
- package/src/engine/intelligence/behavior/composite/ParallelBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/composite/ParallelBehavior.js +1 -1
- package/src/engine/intelligence/behavior/composite/SequenceBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/composite/SequenceBehavior.js +1 -1
- package/src/engine/intelligence/behavior/decorator/AbstractDecoratorBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/decorator/AbstractDecoratorBehavior.js +1 -1
- package/src/engine/intelligence/behavior/decorator/IgnoreFailureBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/decorator/IgnoreFailureBehavior.js +1 -1
- package/src/engine/intelligence/behavior/decorator/InvertStatusBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/decorator/InvertStatusBehavior.js +1 -1
- package/src/engine/intelligence/behavior/decorator/RepeatBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/decorator/RepeatBehavior.js +1 -1
- package/src/engine/intelligence/behavior/decorator/RepeatUntilSuccessBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/decorator/RepeatUntilSuccessBehavior.js +1 -1
- package/src/engine/intelligence/behavior/ecs/BehaviorComponent.d.ts +1 -1
- package/src/engine/intelligence/behavior/ecs/BehaviorComponent.js +1 -1
- package/src/engine/intelligence/behavior/ecs/BehaviorSystem.d.ts +1 -1
- package/src/engine/intelligence/behavior/ecs/BehaviorSystem.js +1 -1
- package/src/engine/intelligence/behavior/ecs/DieBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/ecs/DieBehavior.js +1 -1
- package/src/engine/intelligence/behavior/ecs/EntityBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/ecs/EntityBehavior.js +1 -1
- package/src/engine/intelligence/behavior/ecs/KillBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/ecs/KillBehavior.js +1 -1
- package/src/engine/intelligence/behavior/ecs/SendEventBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/ecs/SendEventBehavior.js +1 -1
- package/src/engine/intelligence/behavior/ecs/WaitForEventBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/ecs/WaitForEventBehavior.js +1 -1
- package/src/engine/intelligence/behavior/primitive/ActionBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/primitive/ActionBehavior.js +1 -1
- package/src/engine/intelligence/behavior/primitive/FailingBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/primitive/FailingBehavior.js +1 -1
- package/src/engine/intelligence/behavior/primitive/PromiseBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/primitive/PromiseBehavior.js +1 -1
- package/src/engine/intelligence/behavior/primitive/SucceedingBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/primitive/SucceedingBehavior.js +1 -1
- package/src/engine/intelligence/behavior/selector/WeightedElement.d.ts +1 -1
- package/src/engine/intelligence/behavior/selector/WeightedElement.js +1 -1
- package/src/engine/intelligence/behavior/selector/WeightedRandomBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/selector/WeightedRandomBehavior.js +1 -1
- package/src/engine/intelligence/behavior/util/BranchBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/util/BranchBehavior.js +1 -1
- package/src/engine/intelligence/behavior/util/DelayBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/util/DelayBehavior.js +1 -1
- package/src/engine/intelligence/behavior/util/LogMessageBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/util/LogMessageBehavior.js +1 -1
- package/src/engine/intelligence/behavior/util/RandomDelayBehavior.d.ts +1 -1
- package/src/engine/intelligence/behavior/util/RandomDelayBehavior.js +1 -1
- package/src/engine/intelligence/blackboard/Blackboard.js +1 -1
- package/src/engine/intelligence/mcts/MonteCarlo.d.ts +1 -1
- package/src/engine/intelligence/mcts/MonteCarlo.js +1 -1
- package/src/engine/intelligence/mcts/MoveEdge.d.ts +1 -1
- package/src/engine/intelligence/mcts/MoveEdge.js +1 -1
- package/src/engine/intelligence/mcts/StateNode.d.ts +1 -1
- package/src/engine/intelligence/mcts/StateNode.js +1 -1
- package/src/engine/intelligence/resource/ActionSequence.d.ts +1 -1
- package/src/engine/intelligence/resource/ActionSequence.js +1 -1
- package/src/engine/intelligence/resource/Resource.d.ts +1 -1
- package/src/engine/intelligence/resource/Resource.js +1 -1
- package/src/engine/intelligence/resource/ResourceAllocation.d.ts +1 -1
- package/src/engine/intelligence/resource/ResourceAllocation.js +1 -1
- package/src/engine/intelligence/resource/ResourceAllocationBid.d.ts +1 -1
- package/src/engine/intelligence/resource/ResourceAllocationBid.js +1 -1
- package/src/engine/intelligence/resource/ResourceAllocationSolver.d.ts +1 -1
- package/src/engine/intelligence/resource/ResourceAllocationSolver.js +1 -1
- package/src/engine/intelligence/resource/StrategicResourceAllocator.d.ts +1 -1
- package/src/engine/intelligence/resource/StrategicResourceAllocator.js +1 -1
- package/src/engine/intelligence/resource/TacticalModule.d.ts +1 -1
- package/src/engine/intelligence/resource/TacticalModule.js +1 -1
- package/src/engine/network/NetworkSession.d.ts +1 -1
- package/src/engine/network/NetworkSession.js +1 -1
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts +1 -1
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +1 -1
- package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts +1 -1
- package/src/engine/network/adapters/TransformInterpolationAdapter.js +1 -1
- package/src/engine/network/adapters/TransformReplicationAdapter.d.ts +1 -1
- package/src/engine/network/adapters/TransformReplicationAdapter.js +1 -1
- package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts +1 -1
- package/src/engine/network/adapters/Vector3InterpolationAdapter.js +1 -1
- package/src/engine/network/core/quantize/quantize_float.d.ts +1 -1
- package/src/engine/network/core/quantize/quantize_float.js +1 -1
- package/src/engine/network/core/quantize/quantize_position.d.ts +1 -1
- package/src/engine/network/core/quantize/quantize_position.js +1 -1
- package/src/engine/network/core/sequence/ack_bitfield.d.ts +1 -1
- package/src/engine/network/core/sequence/ack_bitfield.js +1 -1
- package/src/engine/network/core/sequence/seq16.d.ts +1 -1
- package/src/engine/network/core/sequence/seq16.js +1 -1
- package/src/engine/network/core/sequence/seq32.d.ts +1 -1
- package/src/engine/network/core/sequence/seq32.js +1 -1
- package/src/engine/network/diagnostics/BandwidthMeter.d.ts +1 -1
- package/src/engine/network/diagnostics/BandwidthMeter.js +1 -1
- package/src/engine/network/diagnostics/ReplayLog.d.ts +1 -1
- package/src/engine/network/diagnostics/ReplayLog.js +1 -1
- package/src/engine/network/diagnostics/SyncTest.d.ts +1 -1
- package/src/engine/network/diagnostics/SyncTest.js +1 -1
- package/src/engine/network/ecs/NetworkSystem.d.ts +1 -1
- package/src/engine/network/ecs/NetworkSystem.js +1 -1
- package/src/engine/network/ecs/components/NetworkIdentity.d.ts +1 -1
- package/src/engine/network/ecs/components/NetworkIdentity.js +1 -1
- package/src/engine/network/ecs/serialization/NetworkIdentitySerializationAdapter.d.ts +1 -1
- package/src/engine/network/ecs/serialization/NetworkIdentitySerializationAdapter.js +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.d.ts +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +1 -1
- package/src/engine/network/replication/Replicator.d.ts +1 -1
- package/src/engine/network/replication/Replicator.js +1 -1
- package/src/engine/network/replication/ScopeFilter.d.ts +2 -2
- package/src/engine/network/replication/ScopeFilter.js +2 -2
- package/src/engine/network/sim/ActionLog.d.ts +1 -1
- package/src/engine/network/sim/ActionLog.js +1 -1
- package/src/engine/network/sim/BinaryInterpolationAdapter.d.ts +1 -1
- package/src/engine/network/sim/BinaryInterpolationAdapter.js +1 -1
- package/src/engine/network/sim/InterpolationLog.d.ts +1 -1
- package/src/engine/network/sim/InterpolationLog.js +1 -1
- package/src/engine/network/sim/ReplicatedComponentRegistry.d.ts +1 -1
- package/src/engine/network/sim/ReplicatedComponentRegistry.js +1 -1
- package/src/engine/network/sim/RewindEngine.d.ts +1 -1
- package/src/engine/network/sim/RewindEngine.js +1 -1
- package/src/engine/network/sim/SimAction.d.ts +1 -1
- package/src/engine/network/sim/SimAction.js +1 -1
- package/src/engine/network/sim/SimActionExecutor.d.ts +1 -1
- package/src/engine/network/sim/SimActionExecutor.js +1 -1
- package/src/engine/network/sim/SimActionRegistry.d.ts +1 -1
- package/src/engine/network/sim/SimActionRegistry.js +1 -1
- package/src/engine/network/sim/SmoothingState.js +1 -1
- package/src/engine/network/sim/Snapshotter.d.ts +1 -1
- package/src/engine/network/sim/Snapshotter.js +1 -1
- package/src/engine/network/sim/SpeculationLog.d.ts +1 -1
- package/src/engine/network/sim/SpeculationLog.js +1 -1
- package/src/engine/network/state/Baseline.d.ts +1 -1
- package/src/engine/network/state/Baseline.js +1 -1
- package/src/engine/network/state/ChangedEntitySet.d.ts +1 -1
- package/src/engine/network/state/ChangedEntitySet.js +1 -1
- package/src/engine/network/state/InputRing.d.ts +1 -1
- package/src/engine/network/state/InputRing.js +1 -1
- package/src/engine/network/state/MutationLedger.d.ts +1 -1
- package/src/engine/network/state/MutationLedger.js +1 -1
- package/src/engine/network/state/PriorityAccumulator.d.ts +1 -1
- package/src/engine/network/state/PriorityAccumulator.js +1 -1
- package/src/engine/network/state/ReplicationSlotTable.d.ts +1 -1
- package/src/engine/network/state/ReplicationSlotTable.js +1 -1
- package/src/engine/network/time/AdaptiveRenderDelay.d.ts +1 -1
- package/src/engine/network/time/AdaptiveRenderDelay.js +1 -1
- package/src/engine/network/time/JitterBuffer.d.ts +1 -1
- package/src/engine/network/time/JitterBuffer.js +1 -1
- package/src/engine/network/time/TimeDilation.d.ts +1 -1
- package/src/engine/network/time/TimeDilation.js +1 -1
- package/src/engine/network/time/TimeSync.d.ts +1 -1
- package/src/engine/network/time/TimeSync.js +1 -1
- package/src/engine/network/transport/Channel.d.ts.map +1 -1
- package/src/engine/network/transport/Channel.js +3 -6
- package/src/engine/network/transport/LoopbackTransport.d.ts +1 -1
- package/src/engine/network/transport/LoopbackTransport.js +1 -1
- package/src/engine/network/transport/ReliableCommandPipeline.d.ts +1 -1
- package/src/engine/network/transport/ReliableCommandPipeline.js +1 -1
- package/src/engine/network/transport/Transport.d.ts +1 -1
- package/src/engine/network/transport/Transport.js +1 -1
- package/src/engine/network/transport/adapters/NodeUDPTransport.d.ts +1 -1
- package/src/engine/network/transport/adapters/NodeUDPTransport.js +2 -2
- package/src/engine/network/transport/adapters/SimulatedTransport.d.ts +1 -1
- package/src/engine/network/transport/adapters/SimulatedTransport.js +1 -1
- package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.d.ts +1 -1
- package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.js +1 -1
- package/src/engine/network/transport/adapters/WebSocketTransport.d.ts +1 -1
- package/src/engine/network/transport/adapters/WebSocketTransport.js +1 -1
- package/src/engine/network/transport/adapters/WebTransportTransport.d.ts +1 -1
- package/src/engine/network/transport/adapters/WebTransportTransport.js +1 -1
- package/src/engine/network/transport/fragments/FragmentAssembler.d.ts +1 -1
- package/src/engine/network/transport/fragments/FragmentAssembler.js +1 -1
- package/src/engine/network/transport/fragments/FragmentRetention.d.ts +1 -1
- package/src/engine/network/transport/fragments/FragmentRetention.js +1 -1
- package/src/engine/network/transport/fragments/packet_size.d.ts +1 -1
- package/src/engine/network/transport/fragments/packet_size.js +1 -1
- package/src/engine/simulation/Ticker.d.ts +1 -1
- package/src/engine/simulation/Ticker.js +1 -1
- package/src/engine/sound/SoundEngine.js +1 -1
- package/src/engine/ui/DraggableAspect.d.ts +1 -1
- package/src/engine/ui/DraggableAspect.js +1 -1
- package/src/view/View.js +1 -1
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
# FirstPersonPlayerController — Design
|
|
2
|
+
|
|
3
|
+
> Status: **Draft** — design doc. No implementation yet.
|
|
4
|
+
> Scope: one entity, one body, one camera. No skeleton, no avatar mesh, no sound, no input wiring.
|
|
5
|
+
> Companion files (planned): `FirstPersonPlayerController.js` (component), `FirstPersonPlayerControllerSystem.js` (system), serialization adapter, spec.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Goals
|
|
10
|
+
|
|
11
|
+
The controller has two intertwined goals. Every design decision below should be justified against at least one of them.
|
|
12
|
+
|
|
13
|
+
1. **Feel alive.** Convey weight, inertia, breath, exertion, and consequence. The player should sense a body even when the screen shows only the world.
|
|
14
|
+
2. **Be configurable.** Every parameter that contributes to "feel" must be exposed and tunable per-entity, with sensible defaults. Different characters (heavy soldier vs. light scout vs. drunk NPC) must be expressible by data alone.
|
|
15
|
+
|
|
16
|
+
### Non-goals (explicitly out of scope)
|
|
17
|
+
|
|
18
|
+
- Visual representation of the body (arms, weapon, third-person mesh, shadow).
|
|
19
|
+
- Input wiring (keyboard, gamepad, touch). The controller exposes an **intent surface**; binding is the job of `InputController` or a script.
|
|
20
|
+
- Collision and ground-truth physics resolution. This document assumes a separate physics or capsule-controller layer reports `grounded`, `groundNormal`, and resolves penetration. The controller writes desired velocity / impulses.
|
|
21
|
+
- Skeletal pose evaluation. The controller exports **pose channels** (numeric outputs) intended for a future skeleton system to consume.
|
|
22
|
+
- Sound. The controller exports **events** intended for a future audio system to consume.
|
|
23
|
+
- Networking / rollback. The component is designed to be deterministic per fixed update so this is possible later, but no replication is included.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2. Research notes
|
|
28
|
+
|
|
29
|
+
A condensed reading of public sources informed this design:
|
|
30
|
+
|
|
31
|
+
- **3Cs framework** (Camera, Character, Controls) — these three are interdependent; tuning one warps the others. Treat them as one design surface even though they live in separate components. Joel Nilsson (Massive, *Avatar*) and the gamedevpills writeup are the canonical references.
|
|
32
|
+
- **Mirror's Edge Catalyst camera** (DICE / Erik Söderholm) — the hardest problem is "not feeling like a box that glides around". Solution: animate the camera with body weight, not just track a capsule. Inform jump anticipation, land recovery, breathing, head tilt with the same data the body uses.
|
|
33
|
+
- **Star Citizen FPS stances & breathing** — breathing is an *active subsystem*, not decoration. Breath rate scales with stamina/exertion. Camera sway has 1s rest pauses at full stamina and shrinks to ~0.2s pauses when depleted. Heavy gear amplifies it.
|
|
34
|
+
- **Playtank "First-Person 3Cs: Camera"** — a layered transform hierarchy (Root → Head → Noise → Camera) lets effects compose without coupling. Anti-motion-sickness: keep horizon stable by default, make roll/shake opt-in.
|
|
35
|
+
- **NeoFPS additive transforms** — effects are an additive stack; translation and rotation compose independently so layer order doesn't matter for one but does for the other.
|
|
36
|
+
- **Spring-roll-call** (Daniel Holden) — critically damped springs with half-life parameterization beat lerp for camera smoothing. Under-damped springs are the right tool for landing recovery (they overshoot and ring slightly, which reads as compression).
|
|
37
|
+
- **Coyote time + jump buffer + variable jump height** — three small forgiveness tricks that single-handedly elevate a "stiff" controller. Coyote ~80–120 ms, buffer ~80–150 ms, fall gravity multiplier 1.5–2.5×.
|
|
38
|
+
- **Gait research** — a walk/run cycle is two footfalls per stride; cadence scales roughly with √(g·L) where L is leg length (the pendulum approximation). Heel-strike events happen at the bottom of the vertical bob.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 3. Architectural overview
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
┌────────────────────────────────────────────────────────────────────┐
|
|
46
|
+
│ ENTITY │
|
|
47
|
+
│ │
|
|
48
|
+
│ ┌──────────────────────┐ ┌──────────────┐ ┌────────────────┐ │
|
|
49
|
+
│ │ FirstPersonPlayer- │ │ Transform │ │ Motion │ │
|
|
50
|
+
│ │ Controller │ │ (BODY) │ │ (velocity) │ │
|
|
51
|
+
│ │ │ │ │ │ │ │
|
|
52
|
+
│ │ - config │ │ │ │ │ │
|
|
53
|
+
│ │ - intent (RW) │ └──────────────┘ └────────────────┘ │
|
|
54
|
+
│ │ - state (RO) │ ▲ │
|
|
55
|
+
│ │ - signals (RO) │ │ written by System │
|
|
56
|
+
│ │ - pose channels (RO) │ │ │
|
|
57
|
+
│ └──────────────────────┘ │ │
|
|
58
|
+
│ ▲ │ │
|
|
59
|
+
│ │ ┌─────────┴────────┐ │
|
|
60
|
+
│ │ │ Camera entity │ │
|
|
61
|
+
│ │ │ (child or │ │
|
|
62
|
+
│ │ │ referenced) │ │
|
|
63
|
+
│ │ │ - Transform (EYE)│ │
|
|
64
|
+
│ │ │ - Camera │ │
|
|
65
|
+
│ │ └──────────────────┘ │
|
|
66
|
+
│ │ │
|
|
67
|
+
│ └── intent set by: InputController binding / AI script │
|
|
68
|
+
│ pose channels read by: future skeleton system │
|
|
69
|
+
│ signals consumed by: future audio / FX systems │
|
|
70
|
+
└────────────────────────────────────────────────────────────────────┘
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Two transforms, not one
|
|
74
|
+
|
|
75
|
+
The controller drives **two** transforms:
|
|
76
|
+
|
|
77
|
+
1. **Body transform** — the entity's own `Transform`. Position is the ground-projected foot/capsule origin. Yaw is the body's facing. Pitch and roll on the body are always zero. The body is what a physics system would integrate.
|
|
78
|
+
2. **Eye transform** — a separate transform (camera entity, attached via `TransformAttachment` or held as a referenced entity). Position is body + (height − crouchDip) + (bob + breath + landKick + shake). Rotation is body-yaw × eye-pitch × (lean roll). This is where all the "feel" lives.
|
|
79
|
+
|
|
80
|
+
Why split: body kinematics must remain physics-clean (no jitter into the collider); the camera must be free to lie, dip, and oscillate without affecting collision or AI line-of-sight.
|
|
81
|
+
|
|
82
|
+
### Five processing layers
|
|
83
|
+
|
|
84
|
+
Each frame the system walks these in order:
|
|
85
|
+
|
|
86
|
+
| Layer | Reads | Writes | Cadence |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| **L0 Intent** | (external) | `intent.*` | event-driven |
|
|
89
|
+
| **L1 Locomotion** | `intent`, `state.grounded` | `Motion.velocity`, `Transform` (body) | fixed |
|
|
90
|
+
| **L2 Pose** | `state`, `config` | `pose.*` channels | fixed |
|
|
91
|
+
| **L3 Camera** | `pose`, `state` | eye `Transform`, `Camera.fov` | variable (per-frame) |
|
|
92
|
+
| **L4 Events** | derived from `state` transitions | `signals.*` | fixed (where possible) |
|
|
93
|
+
|
|
94
|
+
L1, L2, L4 run in `fixedUpdate` so they're deterministic and not framerate-coupled. L3 runs in `update` because camera smoothing must be at render rate to avoid judder.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 4. The component: `FirstPersonPlayerController`
|
|
99
|
+
|
|
100
|
+
Standard meep component contract: `typeName`, `toJSON/fromJSON`, `copy/clone/equals/hash`. Fields are grouped into five sub-objects so the public API stays legible.
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
class FirstPersonPlayerController {
|
|
104
|
+
static typeName = "FirstPersonPlayerController"
|
|
105
|
+
|
|
106
|
+
config : FirstPersonConfig // tunables — serialized
|
|
107
|
+
intent : FirstPersonIntent // written by input/AI — transient
|
|
108
|
+
state : FirstPersonState // read-only outputs — transient
|
|
109
|
+
pose : FirstPersonPose // read-only outputs for skeleton — transient
|
|
110
|
+
signals : FirstPersonSignals // Signal<...> instances — transient
|
|
111
|
+
|
|
112
|
+
/** Reference to the eye/camera entity; created on link if absent. */
|
|
113
|
+
eyeEntity : EntityReference
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Only `config` is serialized. Everything else is rebuilt on `link()`.
|
|
118
|
+
|
|
119
|
+
### 4.1 `FirstPersonConfig` — the tunable surface
|
|
120
|
+
|
|
121
|
+
Grouped by feel-axis so designers can browse without scrolling past 80 floats.
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
config = {
|
|
125
|
+
body: {
|
|
126
|
+
height : 1.80 // m, standing eye height
|
|
127
|
+
crouchHeight : 1.10 // m, crouched eye height
|
|
128
|
+
radius : 0.35 // m, capsule radius (for downstream physics)
|
|
129
|
+
mass : 80 // kg, drives bob amplitude & land impact
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
motion: {
|
|
133
|
+
walkSpeed : 3.0 // m/s
|
|
134
|
+
sprintSpeed : 6.0 // m/s
|
|
135
|
+
crouchSpeed : 1.4 // m/s
|
|
136
|
+
airControl : 0.25 // [0..1] how much intent influences air velocity
|
|
137
|
+
groundAccel : 35 // m/s² (~ "snap" of start-up)
|
|
138
|
+
groundDecel : 50 // m/s² when intent zero
|
|
139
|
+
airAccel : 8 // m/s²
|
|
140
|
+
turnRateMax : Inf // rad/s — Inf = instantaneous (mouse-look default)
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
jump: {
|
|
144
|
+
peakHeight : 1.2 // m, the *desired* clean-jump apex
|
|
145
|
+
timeToApex : 0.40 // s — together with peakHeight → gravity & impulse
|
|
146
|
+
coyoteTime : 0.10 // s of grace after walking off a ledge
|
|
147
|
+
bufferTime : 0.12 // s a queued jump remains valid before landing
|
|
148
|
+
fallGravityMult : 2.0 // gravity ×N once vy < 0 or jump released early
|
|
149
|
+
cutGravityMult : 1.8 // gravity ×N if jump released before apex (variable height)
|
|
150
|
+
anticipation: { // squash before take-off
|
|
151
|
+
duration : 0.08 // s
|
|
152
|
+
dipAmount : 0.06 // m, eye dips this much
|
|
153
|
+
curve : "ease-out-quad"
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
landing: {
|
|
158
|
+
softThreshold : 3.0 // m/s vertical — below = soft signal
|
|
159
|
+
hardThreshold : 7.5 // m/s vertical — above = hard signal
|
|
160
|
+
recovery: {
|
|
161
|
+
dipPerVy : 0.012 // m of eye dip per m/s impact velocity
|
|
162
|
+
dipMax : 0.18 // m, clamp
|
|
163
|
+
spring: {
|
|
164
|
+
halfLife : 0.12 // s — under-damped (zeta < 1) for the ring
|
|
165
|
+
zeta : 0.55
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
crouch: {
|
|
171
|
+
transitionTime : 0.18 // s to lerp eye height
|
|
172
|
+
mode : "hold" // "hold" | "toggle" — affects intent semantics
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
bob: {
|
|
176
|
+
// Cadence is derived from speed: freq = stepFreqAtWalk * (speed / walkSpeed) ^ stepFreqExp
|
|
177
|
+
stepFreqAtWalk : 1.8 // Hz (≈ 1.8 footfalls/s, both feet)
|
|
178
|
+
stepFreqExp : 0.9 // sub-linear so sprint isn't frantic
|
|
179
|
+
verticalAmpAtWalk: 0.045 // m
|
|
180
|
+
lateralAmpAtWalk : 0.025 // m — figure-8 (vert is 2x lateral freq)
|
|
181
|
+
ampMassScale : 0.005 // m per kg over reference (80kg)
|
|
182
|
+
rollAtWalkDeg : 0.6 // deg, sympathetic head roll
|
|
183
|
+
// Pattern: vertical(t) = A_v * |sin(πft)| (footfall = trough)
|
|
184
|
+
// lateral (t) = A_l * sin(2πft / 2) (one full cycle = 2 steps)
|
|
185
|
+
// roll (t) = A_r * sin(2πft / 2) (in phase with lateral)
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
breath: {
|
|
189
|
+
// Resting respiration — 14 breaths/min ≈ 0.23 Hz
|
|
190
|
+
rateRestHz : 0.23
|
|
191
|
+
rateMaxHz : 0.70 // panting at full exertion
|
|
192
|
+
amplitudeRestM : 0.004 // tiny — barely visible at rest
|
|
193
|
+
amplitudeMaxM : 0.022 // visible chest heave when exhausted
|
|
194
|
+
pitchAmpRestDeg : 0.10
|
|
195
|
+
pitchAmpMaxDeg : 0.55
|
|
196
|
+
// Optional micro-noise on top of the sine, perlin@0.7Hz, ±20% of amp
|
|
197
|
+
noiseAmount : 0.20
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
exertion: {
|
|
201
|
+
// Single scalar [0..1]. Inputs raise it, time decays it.
|
|
202
|
+
sprintRiseRate : 0.20 // /s while sprinting
|
|
203
|
+
jumpRise : 0.08 // per jump
|
|
204
|
+
idleDecayRate : 0.12 // /s while walking or still
|
|
205
|
+
// Carry-over: after exertion peaks, breath rate decays slower than amplitude
|
|
206
|
+
// (you keep breathing hard for a moment after stopping).
|
|
207
|
+
rateDecayHalfLife: 4.0 // s
|
|
208
|
+
ampDecayHalfLife : 2.0 // s
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
lean: {
|
|
212
|
+
enabled : true // ACCESSIBILITY — make false to disable roll
|
|
213
|
+
maxRollDeg : 2.5 // deg per (lateral m/s² / 9.81)
|
|
214
|
+
spring: { halfLife: 0.18, zeta: 1.0 }
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
fov: {
|
|
218
|
+
base : 70 // deg
|
|
219
|
+
sprintAdd : 6 // deg added when at full sprint speed
|
|
220
|
+
crouchAdd : -3
|
|
221
|
+
smoothHalfLife : 0.20 // s
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
look: {
|
|
225
|
+
pitchMinDeg : -85
|
|
226
|
+
pitchMaxDeg : 85
|
|
227
|
+
invertY : false
|
|
228
|
+
// Sensitivity itself belongs to the input layer, not here.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Every nested object is itself a normal object that round-trips through `toJSON`. Serialization adapter just walks the tree.
|
|
234
|
+
|
|
235
|
+
### 4.2 `FirstPersonIntent` — the control surface
|
|
236
|
+
|
|
237
|
+
This is what input bindings or AI scripts write to. The system reads it once per fixed update and zeroes the *consumable* bits (`look`).
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
intent = {
|
|
241
|
+
move : Vector2 // [-1..1, -1..1] — forward+/right+ in body-local (state)
|
|
242
|
+
look : Vector2 // accumulated delta yaw/pitch in radians (consumed each fixed step)
|
|
243
|
+
jump : boolean // HOLD STATE — true while button held; edge detected inside system
|
|
244
|
+
crouch : boolean // HOLD STATE — meaning depends on config.crouch.mode
|
|
245
|
+
sprint : boolean // HOLD STATE — sprint applies only when moveY > 0 and grounded
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Three input semantics live here:
|
|
250
|
+
- **Delta**: `look` — accumulated between fixed updates, zeroed on consume.
|
|
251
|
+
- **State (hold)**: `move`, `crouch`, `sprint`, `jump` — latched. Always reflects the current physical button/stick state.
|
|
252
|
+
- **Edge**: detected internally by the system from `jump` (and from `crouch` when `config.crouch.mode === "toggle"`). The intent surface never exposes "I just pressed it" — that's the system's bookkeeping. This is what guarantees a held jump button doesn't auto-repeat.
|
|
253
|
+
|
|
254
|
+
That makes all three common input topologies trivial to wire:
|
|
255
|
+
- Mouse + keyboard: mouse writes `look += delta`, WASD writes `move`, space-down/space-up writes `jump`.
|
|
256
|
+
- Gamepad: right stick writes `look = stick * sensitivity * dt`, left stick writes `move`, A-down/A-up writes `jump`.
|
|
257
|
+
- AI: scripts write the same fields directly, no input device required. Scripts that want a "single jump" set `jump = true` for one fixed step then `jump = false`.
|
|
258
|
+
|
|
259
|
+
### 4.3 `FirstPersonState` — derived, read-only
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
state = {
|
|
263
|
+
// Grounding
|
|
264
|
+
grounded : boolean
|
|
265
|
+
timeSinceGrounded : number // s — feeds coyote time
|
|
266
|
+
groundNormal : Vector3 // unit, set by physics layer; default Up
|
|
267
|
+
surfaceTag : string? // optional, "grass" | "wood" | ... for FX
|
|
268
|
+
|
|
269
|
+
// Locomotion
|
|
270
|
+
speed : number // m/s, horizontal magnitude of Motion.velocity
|
|
271
|
+
speedNormalized : number // [0..1] vs sprintSpeed — drives bob amp/freq
|
|
272
|
+
moveMode : enum // Idle | Walk | Sprint | Crouch | Air
|
|
273
|
+
|
|
274
|
+
// Vertical
|
|
275
|
+
verticalSpeed : number // signed m/s
|
|
276
|
+
airborneTime : number // s since leaving ground
|
|
277
|
+
fallDistance : number // m fallen since last grounded
|
|
278
|
+
|
|
279
|
+
// Jump bookkeeping
|
|
280
|
+
jumpBufferRemaining: number // s — set when intent.jump fires; consumed on land
|
|
281
|
+
inJumpAnticipation : boolean // true during the squash window
|
|
282
|
+
isVariableJumpCut : boolean // true if button released before apex
|
|
283
|
+
|
|
284
|
+
// Exertion & breath
|
|
285
|
+
exertion : number // [0..1], smoothed
|
|
286
|
+
breathPhase : number // [0..1) — fraction through current breath cycle
|
|
287
|
+
breathRateHz : number // current actual rate
|
|
288
|
+
breathAmplitudeM : number // current actual amplitude
|
|
289
|
+
|
|
290
|
+
// Stride
|
|
291
|
+
stridePhase : number // [0..1) — fraction through 2-step cycle
|
|
292
|
+
stepCount : integer // monotonic; useful for "every Nth step" effects
|
|
293
|
+
|
|
294
|
+
// Pose
|
|
295
|
+
eyeHeight : number // current m above body origin (lerped during crouch)
|
|
296
|
+
|
|
297
|
+
// Tilt
|
|
298
|
+
leanRollRad : number // current eye roll
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### 4.4 `FirstPersonPose` — channels for a skeleton system
|
|
303
|
+
|
|
304
|
+
These are the *outputs* a future humanoid rig should drive itself from. Designed so the same controller can later feed:
|
|
305
|
+
- a first-person arms rig (arms + weapon),
|
|
306
|
+
- a third-person full-body rig,
|
|
307
|
+
- a network-replicated remote rig.
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
pose = {
|
|
311
|
+
// World-space body
|
|
312
|
+
rootPosition : Vector3
|
|
313
|
+
rootYawRad : number // body facing
|
|
314
|
+
|
|
315
|
+
// Head decoupled from body (the "look")
|
|
316
|
+
headYawRad : number // = body yaw for most controllers
|
|
317
|
+
headPitchRad : number
|
|
318
|
+
headRollRad : number // includes lean
|
|
319
|
+
|
|
320
|
+
// Locomotion phase — for animation playback
|
|
321
|
+
locomotionPhase : number // [0..1), synced to stridePhase
|
|
322
|
+
locomotionSpeed : number // m/s, for blend tree
|
|
323
|
+
locomotionStrafe : number // [-1..1], for strafe-blend space
|
|
324
|
+
|
|
325
|
+
// Action state — for state machine driving
|
|
326
|
+
actionState : enum // Grounded | Anticipating | Airborne | Landing
|
|
327
|
+
crouchAmount : number // [0..1] interpolation between standing/crouched poses
|
|
328
|
+
aimPitch : number // = headPitchRad, surfaced separately for IK
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The pose channels are intentionally *redundant* with `state` — they're a stable contract for downstream systems, even if `state` internals churn.
|
|
333
|
+
|
|
334
|
+
### 4.5 `FirstPersonSignals` — event surface
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
signals = {
|
|
338
|
+
onFootStep : Signal<{ side: "L"|"R", speed: number, surfaceTag: string? }>
|
|
339
|
+
onJumpStart : Signal<{ peakHeight: number }> // fires at impulse, after anticipation
|
|
340
|
+
onJumpApex : Signal<{}>
|
|
341
|
+
onLeaveGround: Signal<{ reason: "jump"|"fall" }>
|
|
342
|
+
onLand : Signal<{ verticalSpeed: number, kind: "soft"|"hard" }>
|
|
343
|
+
onCrouchEnter: Signal<{}>
|
|
344
|
+
onCrouchExit : Signal<{}>
|
|
345
|
+
onSprintStart: Signal<{}>
|
|
346
|
+
onSprintStop : Signal<{}>
|
|
347
|
+
onBreathIn : Signal<{ amplitude: number, rateHz: number }> // peak inhale
|
|
348
|
+
onBreathOut : Signal<{ amplitude: number, rateHz: number }> // peak exhale
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
All signals fire from `fixedUpdate` so listeners see consistent state. `onFootStep` fires twice per stride at the troughs of vertical bob. The `side` flips each footstep.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## 5. The system: `FirstPersonPlayerControllerSystem`
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
class FirstPersonPlayerControllerSystem extends System {
|
|
360
|
+
dependencies = [FirstPersonPlayerController, Transform, Motion]
|
|
361
|
+
components_used = [
|
|
362
|
+
spec(Transform, RW),
|
|
363
|
+
spec(Motion, RW),
|
|
364
|
+
spec(Camera, RW), // on the eye entity
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
fixedUpdate(dt) { /* L1, L2, L4 — deterministic */ }
|
|
368
|
+
update(dt) { /* L3 — camera composition at render rate */ }
|
|
369
|
+
|
|
370
|
+
link(controller, transform, motion, entity) { /* spawn eye entity if missing, build signals, snapshot defaults */ }
|
|
371
|
+
unlink(controller, transform, motion, entity) { /* tear down eye entity, clear signals */ }
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### 5.1 `fixedUpdate` — pseudocode
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
for each entity (controller, bodyTransform, motion):
|
|
379
|
+
// ---- L1: Locomotion -------------------------------------------------
|
|
380
|
+
consumeLookDelta(controller.intent.look) → apply to bodyYaw, eyePitch (clamped)
|
|
381
|
+
desiredSpeed = pickSpeed(controller, state.moveMode)
|
|
382
|
+
desiredVel = bodyLocal(intent.move) * desiredSpeed
|
|
383
|
+
accel = state.grounded ? config.motion.groundAccel : config.motion.airAccel
|
|
384
|
+
motion.velocity.xz = stepTowards(motion.velocity.xz, desiredVel, accel * dt)
|
|
385
|
+
|
|
386
|
+
// Jump
|
|
387
|
+
if intent.jump consumed:
|
|
388
|
+
state.jumpBufferRemaining = config.jump.bufferTime
|
|
389
|
+
state.jumpBufferRemaining -= dt
|
|
390
|
+
|
|
391
|
+
canJump = (state.grounded || state.timeSinceGrounded < config.jump.coyoteTime)
|
|
392
|
+
&& state.jumpBufferRemaining > 0
|
|
393
|
+
if canJump and not state.inJumpAnticipation:
|
|
394
|
+
beginJumpAnticipation() // schedules impulse after anticipation.duration
|
|
395
|
+
signals.onJumpStart.send(...)
|
|
396
|
+
|
|
397
|
+
integrateAnticipationAndImpulse(dt)
|
|
398
|
+
applyGravity(dt, with fallGravityMult / cutGravityMult)
|
|
399
|
+
integrate(bodyTransform.position += motion.velocity * dt)
|
|
400
|
+
|
|
401
|
+
// Ground detection feedback (set externally by physics layer or assumed flat)
|
|
402
|
+
updateGroundedFromExternalContact()
|
|
403
|
+
|
|
404
|
+
// ---- L2: Pose -------------------------------------------------------
|
|
405
|
+
updateExertion(dt)
|
|
406
|
+
updateBreath(dt) // advances breathPhase, computes amp/rate
|
|
407
|
+
updateStride(dt) // advances stridePhase when grounded & moving
|
|
408
|
+
updateCrouch(dt) // lerps eyeHeight
|
|
409
|
+
updateLeanTarget(dt) // computes target roll from lateral accel; springed in L3
|
|
410
|
+
|
|
411
|
+
// ---- L4: Events -----------------------------------------------------
|
|
412
|
+
if stridePhase wrapped past 0 or 0.5:
|
|
413
|
+
signals.onFootStep.send({ side: nextSide(), ... })
|
|
414
|
+
detectGroundedEdges() // sends onLeaveGround / onLand
|
|
415
|
+
detectJumpApex()
|
|
416
|
+
detectBreathPeaks() // sends onBreathIn / onBreathOut
|
|
417
|
+
detectCrouchEdges()
|
|
418
|
+
detectSprintEdges()
|
|
419
|
+
|
|
420
|
+
// Publish pose channels
|
|
421
|
+
writePoseChannels()
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### 5.2 `update` (variable rate) — camera composition
|
|
425
|
+
|
|
426
|
+
```
|
|
427
|
+
for each entity:
|
|
428
|
+
eyeT = eye transform
|
|
429
|
+
body = body transform
|
|
430
|
+
|
|
431
|
+
// Base eye position (body local)
|
|
432
|
+
base = (0, state.eyeHeight, 0)
|
|
433
|
+
|
|
434
|
+
// Additive offsets — composed in the body's local space, then world-transformed
|
|
435
|
+
bobOffset = computeBob(state.stridePhase, state.speedNormalized, config)
|
|
436
|
+
breathOffset = computeBreath(state.breathPhase, state.breathAmplitudeM, config)
|
|
437
|
+
landOffset = landingSpring.evaluate(dt) // under-damped, decays to 0
|
|
438
|
+
anticipOffset= state.inJumpAnticipation ? anticipationCurve.eval() : 0
|
|
439
|
+
shakeOffset = externalShakeStack.evaluate(dt) // future: gunfire, explosions
|
|
440
|
+
|
|
441
|
+
localEyePos = base + (bobOffset + breathOffset + landOffset + anticipOffset + shakeOffset)
|
|
442
|
+
eyeT.position = body.position + bodyRotation * localEyePos
|
|
443
|
+
|
|
444
|
+
// Rotation: yaw from body, pitch from look, roll from lean + bob
|
|
445
|
+
rollTotal = leanSpring.evaluate(dt) + bobRoll(state.stridePhase, config)
|
|
446
|
+
eyeT.rotation = quatYaw(body.yaw) * quatPitch(state.eyePitch) * quatRoll(rollTotal)
|
|
447
|
+
|
|
448
|
+
// FOV
|
|
449
|
+
fovTarget = config.fov.base
|
|
450
|
+
+ lerp(0, config.fov.sprintAdd, state.speedNormalized * sprintness)
|
|
451
|
+
+ (state.moveMode == Crouch ? config.fov.crouchAdd : 0)
|
|
452
|
+
camera.fov = springTowards(camera.fov, fovTarget, config.fov.smoothHalfLife, dt)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### 5.3 Key formulas
|
|
456
|
+
|
|
457
|
+
**Jump from peak height & time-to-apex** (lets designers think in m and seconds, not Newtons):
|
|
458
|
+
```
|
|
459
|
+
g = 2 * peakHeight / (timeToApex²)
|
|
460
|
+
initialVy = 2 * peakHeight / timeToApex
|
|
461
|
+
```
|
|
462
|
+
On variable-height cut: switch gravity to `g * cutGravityMult` once button released *and* `vy > 0`.
|
|
463
|
+
On descent: switch gravity to `g * fallGravityMult` once `vy <= 0`.
|
|
464
|
+
This matches the classic Sonic/Mario "fast fall" feel.
|
|
465
|
+
|
|
466
|
+
**Stride frequency from speed**:
|
|
467
|
+
```
|
|
468
|
+
f = config.bob.stepFreqAtWalk
|
|
469
|
+
* (max(speed, ε) / config.motion.walkSpeed) ^ config.bob.stepFreqExp
|
|
470
|
+
```
|
|
471
|
+
At rest `f → 0` and bob amplitude clamps to 0, so the player stops bobbing when they stop.
|
|
472
|
+
|
|
473
|
+
**Bob shape** (vertical uses `|sin|` so each footfall is a trough; horizontal uses half-rate sine so left/right alternate):
|
|
474
|
+
```
|
|
475
|
+
phase = stridePhase * 2π // one full cycle per 2 steps
|
|
476
|
+
vert = -A_v * |sin(phase)| * speedNormalized
|
|
477
|
+
lateral = A_l * sin(phase / 2) * speedNormalized // half-rate
|
|
478
|
+
roll = A_r * sin(phase / 2) * speedNormalized // in phase with lateral
|
|
479
|
+
```
|
|
480
|
+
Sign on `vert` is negative so the eye dips on footfall — this is what reads as "weight".
|
|
481
|
+
|
|
482
|
+
**Footstep detection**: emit `onFootStep` when `stridePhase` crosses `0` (right foot) or `0.5` (left foot), but only if `state.grounded && speed > minStepSpeed`. The boundary check uses the *previous* phase so it fires exactly once even at huge dt.
|
|
483
|
+
|
|
484
|
+
**Breath cycle**:
|
|
485
|
+
```
|
|
486
|
+
breathRateHz = lerp(rateRest, rateMax, exertion) decaying with rateDecayHalfLife
|
|
487
|
+
breathAmpM = lerp(ampRest, ampMax, exertion) decaying with ampDecayHalfLife
|
|
488
|
+
breathPhase += breathRateHz * dt; wrap to [0,1)
|
|
489
|
+
|
|
490
|
+
verticalBreath = -breathAmpM * sin(2π * breathPhase) * (1 + noiseAmount * perlin(t*0.7))
|
|
491
|
+
pitchBreath = breathPitchAmp * cos(2π * breathPhase) // gentle nod, 90° out of phase
|
|
492
|
+
```
|
|
493
|
+
Negative `vertical` so peak exhale dips the eye slightly (chest collapses). `onBreathIn` fires at phase `0.25`, `onBreathOut` at `0.75`.
|
|
494
|
+
|
|
495
|
+
**Exertion update**:
|
|
496
|
+
```
|
|
497
|
+
rise = (sprintActive ? sprintRiseRate : 0) + jumpRiseDelta(this frame)
|
|
498
|
+
fall = idleDecayRate * (1 - rise > 0 ? 0 : 1)
|
|
499
|
+
exertion = clamp(exertion + (rise - fall) * dt, 0, 1)
|
|
500
|
+
```
|
|
501
|
+
Then `breathRateHz` and `breathAmpM` track `exertion` through *separate* exponential lags — rate hangs around longer than amplitude, matching how real recovery works (you breathe fast for a while after the chest has stopped heaving).
|
|
502
|
+
|
|
503
|
+
**Critically damped spring** (used for FOV, lean, crouch height):
|
|
504
|
+
```
|
|
505
|
+
y = halfLife / 0.6931
|
|
506
|
+
α = exp(-y * dt)
|
|
507
|
+
β = (x - target)
|
|
508
|
+
v = (v - β * y) * α
|
|
509
|
+
x = target + (β + v * dt / y) * α
|
|
510
|
+
```
|
|
511
|
+
For landing recovery, use the same form with `zeta = 0.55` so it overshoots once before settling.
|
|
512
|
+
|
|
513
|
+
### 5.4 Edge cases and rules
|
|
514
|
+
|
|
515
|
+
- **Look-pitch clamp** is *non-wrapping*: the pitch is stored as a separate scalar so it cannot accidentally roll over. Yaw on the body wraps freely.
|
|
516
|
+
- **Crouch under a ceiling**: if `intent.crouch` releases but the entity cannot stand up (raycast collides), the controller *holds* crouch and exposes `state.crouchBlocked = true`. Standing happens automatically when clear. This rule lives in the controller because it's universal; the raycast itself is a service-injection point.
|
|
517
|
+
- **Sprint requires forward intent**: sideways-only or backwards movement should not consume exertion at sprint rates. Specifically `sprint = intent.sprint && intent.move.y > 0.5 && state.grounded`.
|
|
518
|
+
- **No bob in air**: stride does not advance while `!grounded`. Phase freezes so landing doesn't re-fire a stale footstep.
|
|
519
|
+
- **Jump cancel during anticipation**: if grounded becomes false during anticipation (e.g. ground pulled out), drop the queued impulse and emit `onLeaveGround{reason:"fall"}` without `onJumpStart`.
|
|
520
|
+
- **Disable when paused**: the system should respect a `controller.enabled` flag (or scene pause) and freeze `update` but still allow `fixedUpdate` to integrate gravity if desired. This is a configuration concern, not behavior — exposed via the existing engine pause mechanism.
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## 6. Accessibility & motion sickness
|
|
525
|
+
|
|
526
|
+
Three settings every player should be able to toggle (defaults shown):
|
|
527
|
+
|
|
528
|
+
| Setting | Default | Effect when off |
|
|
529
|
+
|---|---|---|
|
|
530
|
+
| `config.lean.enabled` | true | No roll on strafe |
|
|
531
|
+
| `config.bob.verticalAmpAtWalk` | (nonzero) | Set to 0 to disable bob |
|
|
532
|
+
| `config.fov.sprintAdd` | 6 | Set to 0 to disable FOV kick (a known nausea trigger) |
|
|
533
|
+
|
|
534
|
+
The Playtank article calls these out specifically — keeping the horizon flat and the FOV stable accommodates a meaningful fraction of players who otherwise can't play first-person at all. They are exposed as plain config knobs, not buried.
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## 7. Integration points (deferred work)
|
|
539
|
+
|
|
540
|
+
These don't ship with the controller but the design preserves a clean attachment surface for each:
|
|
541
|
+
|
|
542
|
+
| Future system | How it hooks in |
|
|
543
|
+
|---|---|
|
|
544
|
+
| **Capsule physics / collision** | Reads `Motion.velocity` and resolves; writes `state.grounded`, `state.groundNormal`, `state.surfaceTag`. Suggested via a small `GroundContact` component the physics system populates and the controller reads. |
|
|
545
|
+
| **First-person arms rig** | Reads `pose.headPitchRad`, `pose.locomotionPhase`, `state.actionState`. Driven by an animation graph keyed to the same stride phase the bob uses, so feet animate in sync with footstep events. |
|
|
546
|
+
| **Third-person body rig** | Reads `pose.rootYawRad`, `pose.locomotionSpeed/Strafe`, `pose.crouchAmount`, `state.actionState`. Built around a standard blend-space. |
|
|
547
|
+
| **Sound** | Subscribes to `signals.onFootStep` (modulated by `surfaceTag`), `onLand` (kind = soft/hard), `onJumpStart`, `onBreathIn/Out` (volume = amplitude). |
|
|
548
|
+
| **Camera shake from external sources** (gunfire, explosions) | Pushes into the `externalShakeStack` on the controller — an additive transform list. The controller's L3 evaluates it; sources only push. |
|
|
549
|
+
| **Stamina / damage / status** | A separate component can write into `controller.config.exertion.*` at runtime — e.g. an injury raises `idleDecayRate` to a negative value (exertion can never recover fully). |
|
|
550
|
+
| **Networking** | Because L1+L2+L4 are deterministic in `fixedUpdate` and the intent surface is small, the controller can be rolled back / replayed by writing intent and re-running fixed updates. No design changes needed. |
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## 8. File layout
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
engine/control/first-person/
|
|
558
|
+
DESIGN.md (this file)
|
|
559
|
+
FirstPersonPlayerController.js (component class)
|
|
560
|
+
FirstPersonPlayerControllerSystem.js (system class)
|
|
561
|
+
FirstPersonPlayerControllerSerializationAdapter.js
|
|
562
|
+
FirstPersonPlayerController.spec.js (component contract)
|
|
563
|
+
FirstPersonPlayerControllerSystem.spec.js (system behavior)
|
|
564
|
+
math/
|
|
565
|
+
criticallyDampedSpring.js
|
|
566
|
+
underDampedSpring.js
|
|
567
|
+
computeJumpFromApex.js
|
|
568
|
+
pose/
|
|
569
|
+
FirstPersonPose.js (the pose-channel struct)
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Math helpers live as standalone pure functions so they're independently testable and reusable.
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## 9. Resolved design decisions
|
|
577
|
+
|
|
578
|
+
The open questions from the previous draft are now answered:
|
|
579
|
+
|
|
580
|
+
1. **Body pitch** — **No pitch on the body, ever.** Body holds only yaw. Pitch lives on the eye. Standard FPS convention; keeps physics integration clean.
|
|
581
|
+
2. **Eye representation** — **Separate entity** with its own `Transform` + `Camera`, parented to the body via `TransformAttachment`. Holds the door open for vehicle-mount / turret-style reparenting later without refactor.
|
|
582
|
+
3. **Look intent shape** — **`Vector2` delta** (yaw on `x`, pitch on `y`). Matches mouse semantics; yaw-only sources (turret-style AI) just write `(x, 0)`.
|
|
583
|
+
4. **Bob curve evaluation** — **Runtime sine.** Cheap on modern CPUs. No LUT.
|
|
584
|
+
5. **Look processing rate** — **Inside `fixedUpdate`.** Determinism wins. Input bindings accumulate look delta into `intent.look` (a `Vector2`) between fixed updates; the system consumes and zeroes it once per fixed step. Cost: at very low fixed rates (e.g. 30 Hz) mouse feel becomes slightly chunky; mitigation if it ever bites is to raise `fixedUpdateStepSize` from default ~60 Hz to 120 Hz for entities with this controller, *not* to move look back to render rate.
|
|
585
|
+
|
|
586
|
+
### Jump-button repeat (added requirement)
|
|
587
|
+
|
|
588
|
+
**Holding the jump button must not produce repeated jumps.** A jump must consume an explicit *button-down edge*; holding the button after a jump produces no further jumps until the button is released and pressed again.
|
|
589
|
+
|
|
590
|
+
Implementation:
|
|
591
|
+
|
|
592
|
+
- `intent.jump` is a **hold-state boolean** (true while the button is held). The input binding sets it on key-down and clears it on key-up.
|
|
593
|
+
- The system tracks `_jumpButtonWasDown` internally. A jump is *requested* only on the false→true edge, not while the value remains true.
|
|
594
|
+
- Pressing space then holding it across grounded → airborne → grounded transitions produces exactly **one** jump. The player must release and re-press to jump again.
|
|
595
|
+
- Auto-repeat from the OS is already filtered by `KeyboardDevice` (`event.repeat` check), so the edge fires cleanly once per physical press.
|
|
596
|
+
- Bonus: the same edge gives us **variable jump height** — the true→false edge (release) cuts gravity on the ascending portion of the jump. Two birds, one signal.
|
|
597
|
+
|
|
598
|
+
The same hold-state-plus-edge pattern is used for crouch (when `config.crouch.mode === "toggle"`); when in `"hold"` mode the held value is used directly.
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
## 10. Sources
|
|
603
|
+
|
|
604
|
+
- gamedevpills — [The 3Cs Framework: Character, Camera, Controls](https://www.gamedevpills.com/p/the-3cs-framework-character-camera)
|
|
605
|
+
- Playtank — [First-Person 3Cs: Camera](https://playtank.io/2023/05/12/first-person-3cs-camera/)
|
|
606
|
+
- Roberts Space Industries — [Design Notes: FPS Stances & Breathing](https://robertsspaceindustries.com/en/comm-link/engineering/14653-Design-Notes-FPS-Stances-Breathing)
|
|
607
|
+
- NeoFPS — [Additive Transforms and Effects](https://docs.neofps.com/manual/fpcam-additive-transforms-and-effects.html)
|
|
608
|
+
- Daniel Holden — [Spring-It-On: The Game Developer's Spring-Roll-Call](https://theorangeduck.com/page/spring-roll-call)
|
|
609
|
+
- Allen Chou — [Game Math: Precise Control over Numeric Springing](https://allenchou.net/2015/04/game-math-precise-control-over-numeric-springing/)
|
|
610
|
+
- Ryan Juckett — [Damped Springs](https://www.ryanjuckett.com/damped-springs/)
|
|
611
|
+
- GDC Vault — [Animation Bootcamp: Giving Purpose to First-Person Animation](https://www.gdcvault.com/play/1017633/Animation-Bootcamp-Giving-Purpose-to)
|
|
612
|
+
- GDC Vault — [The Art of First Person Animation for Destiny](https://www.gdcvault.com/play/1022297/The-Art-of-First-Person)
|
|
613
|
+
- Palos Publishing — [Simulating Breathing and Idle Motion Procedurally](https://palospublishing.com/simulating-breathing-and-idle-motion-procedurally/)
|
|
614
|
+
- Ketra Games — [Coyote Time and Jump Buffering](https://www.ketra-games.com/2021/08/coyote-time-and-jump-buffering.html)
|
|
615
|
+
- Opsive — [First Person Lean](https://opsive.com/support/documentation/ultimate-character-controller/character/abilities/included-abilities/first-person-lean/) and [Character Foot Effects](https://opsive.com/support/documentation/ultimate-character-controller/surface-system/character-foot-effects/)
|
|
616
|
+
- CTRL500 — [Mirror's Edge Catalyst: Keeping the camera in your face](https://ctrl500.com/developers-corner/mirrors-edge-catalyst-keeping-the-camera-in-your-face/) (DICE / Erik Söderholm)
|