Taxi demo update with sedan
This commit is contained in:
@@ -264,8 +264,49 @@ cd demos/demo-taxi
|
||||
./demoTaxi --headless --exit-after-first-frame
|
||||
# ...or headless end-to-end check of the inherited scene contract:
|
||||
./demoTaxi --headless --test-switch
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use door,
|
||||
# Escape = pause menu (frees the cursor).
|
||||
# ...or headless sedan drive test (spawn, enter, drive, steer, exit —
|
||||
# plus a passenger-seat leg: no PlayerControlledComponent, W does not
|
||||
# drive, and the exit must land on the occupied seat's side of the car)
|
||||
# on the flat vehicle test scene:
|
||||
./demoTaxi --headless --test-vehicle-drive demo_scene_vehicletest.json
|
||||
# ...same test on the exterior scene (streaming terrain, sedan_taxi):
|
||||
./demoTaxi --headless --test-vehicle-drive
|
||||
# ...or the quantitative handling check (W to ~15 m/s, then full lock
|
||||
# for 150 frames; PASS needs >= 45 deg of yaw - catches the
|
||||
# "drives like it's on ice" regression):
|
||||
./demoTaxi --headless --test-vehicle-turn
|
||||
# NOTE: always pass --headless to the tests — without it the frame
|
||||
# delta is wall time (a few FPS under xvfb), so the fixed-length
|
||||
# W-hold drives the sedan clear off the test floor and the exit-spot
|
||||
# raycast legitimately fails.
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use door /
|
||||
# sit in the seat of the sedan door you used (driver = front-left,
|
||||
# only the driver seat drives), hold E = exit the vehicle, Space =
|
||||
# handbrake while driving, Escape = pause menu (frees the cursor).
|
||||
#
|
||||
# Phase 1 sedans: sedan1/sedan_plain are the hand-modeled
|
||||
# sedan-v2.blend via the convention-based generator
|
||||
# assets/blender/scripts/gen_vehicle.py (VEHICLES_GEN_SRC in
|
||||
# assets/blender/vehicles/CMakeLists.txt) - seat_* empties mark the
|
||||
# character root pins, the driving state plays the sitting-vehicle
|
||||
# animation (no offsets, seatCharacterScale 1.0); see
|
||||
# assets/blender/vehicles/CAR_MODELING.md. sedan_taxi is still the
|
||||
# older procedural gen_taxi_assets.py car (sitting pose at
|
||||
# seatCharacterScale 0.85) pending a v2-based taxi model. The exterior
|
||||
# scene parks sedan_taxi and sedan_plain near the spawn point; each
|
||||
# door child has an actuator with a vehicle_seat_* action (driver,
|
||||
# passenger, rl, rr) that seats the player at that door's seat via the
|
||||
# vehicle_enter event (vehicle resolved by proximity).
|
||||
# Visual tuning aids (work in any scene):
|
||||
./demoTaxi demo_scene_exterior.json --force-pos 4006 11 3972 90 -10 \
|
||||
--debug-enter-vehicle sedan_taxi --screenshot 300 /tmp/seat.png
|
||||
# --force-pos x y z yawDeg [pitchDeg] teleport the player once,
|
||||
# re-apply the TPS yaw/pitch
|
||||
# --debug-enter-vehicle name seat the player in the named
|
||||
# vehicle (default driver seat)
|
||||
# --debug-enter-seat seat pick the seat (driver,
|
||||
# passenger, rl, rr)
|
||||
# --screenshot N path dump frame N and quit
|
||||
|
||||
|
||||
# Road wedge/segment self-intersection regression test (no scene needed,
|
||||
@@ -988,13 +1029,38 @@ collider) gets a vehicle constraint on its rigid body;
|
||||
/ `inputBrake` / `inputHandBrake` into Jolt (with the Jolt sample's
|
||||
brake-before-reverse rule) and `postPhysicsUpdate()` moves wheel visual
|
||||
child nodes (created from `wheelMeshName`) to the constraint's wheel
|
||||
transforms. Both hooks are called from `EditorApp` around the physics
|
||||
step. Any number of vehicles per scene; removal of the component or
|
||||
transforms — right-side wheels (chassis -X) get an extra 180 deg flip
|
||||
about the local forward axis, because Jolt reports every wheel with the
|
||||
same axle direction and a single shared wheel mesh would otherwise show
|
||||
its inner face outward on the right (no mirrored mesh needed). Both
|
||||
hooks are called from `EditorApp` around the physics step. Any number of vehicles per scene; removal of the component or
|
||||
the body tears the constraint down safely.
|
||||
|
||||
`prePhysicsUpdate()` also applies anti-two-wheel righting: when the
|
||||
chassis tilt passes ~35 deg (up to ~80 deg — a car on its side or
|
||||
roof is left alone) the wrapper's `addTorque` applies a torque about
|
||||
the chassis-up x world-up axis proportional to sin(tilt), and above
|
||||
~5 deg of tilt the angular velocity along that axis is damped so the
|
||||
car does not rock on its suspension for seconds. A high-grip car
|
||||
still trips past 45 deg transiently in a full-lock turn, but it can
|
||||
no longer get stuck driving on two wheels; regression check:
|
||||
`demoTaxi --headless --test-vehicle-righting`.
|
||||
|
||||
`VehicleComponent` fields (serialized as the `vehicle` scene key,
|
||||
editable via `ui/VehicleEditor`): `maxTorque`, `maxPitchRollAngleDeg`,
|
||||
`seatOffset`, `cameraDistance` / `cameraHeight` (TPS boom while driving),
|
||||
`tireFrictionLongitudinal` / `tireFrictionLateral` (multipliers over
|
||||
the Jolt default tire friction curves, peak 1.2, before Jolt combines
|
||||
them with ground friction via sqrt(tire * ground) — 1.0 drives like
|
||||
wet ice at speed, sedans use 1.3 / 3.0), `antiRollBarStiffness`
|
||||
(N/m per axle pair of Jolt VehicleAntiRollBar, 0 = none; sedans use
|
||||
2000),
|
||||
`seats` (a list of named chassis-space seats, each with `name`,
|
||||
`offset` and a `driver` flag — only driver seats get driving controls;
|
||||
when empty the legacy `seatOffset` acts as the single driver seat),
|
||||
`seatCharacterScale` (uniform scale applied to the character node
|
||||
while seated, restored on exit — < 1 shrinks the stock chair-sit pose
|
||||
to fit a car cabin), `cameraDistance` / `cameraHeight` (TPS boom while
|
||||
driving),
|
||||
`rearSteer` (flips the steer sign for rear-steered vehicles like the
|
||||
forklift, so D still turns the nose right), `engineBrake` (brake factor
|
||||
applied automatically when coasting above 0.5 m/s with no throttle —
|
||||
@@ -1160,37 +1226,54 @@ without one).
|
||||
Game-mode-only system (`systems/VehicleControllerSystem.{hpp,cpp}`) that
|
||||
makes vehicles drivable by the player:
|
||||
|
||||
- **Enter**: an actuator action (e.g. `vehicle_drive` in the sokoban demo)
|
||||
runs a `luaTask` node `vehicleEnter` (registered in
|
||||
`lua-scripts/data2.lua`) which sends the `vehicle_enter` event with a
|
||||
`vehicle=<entity name>` param; the system subscribes on the `EventBus`
|
||||
(`enterVehicle(entity)` is also directly callable by tests). Entering
|
||||
disables the player character's physics capsule
|
||||
(`CharacterSystem::disablePhysics`), pins its node to
|
||||
`VehicleComponent::seatOffset` (re-pinned every frame from
|
||||
- **Enter**: an actuator action (e.g. `vehicle_drive` in the sokoban demo,
|
||||
`vehicle_seat_*` in the taxi demo) runs a `luaTask` node `vehicleEnter`
|
||||
(registered in `lua-scripts/data2.lua`) which sends the `vehicle_enter`
|
||||
event with a `vehicle=<entity name>` param (empty = nearest vehicle
|
||||
within 5 m of the player) and an optional `seat=<seat name>` param
|
||||
(empty = driver seat, else first free seat); the system subscribes on
|
||||
the `EventBus` (`enterVehicle(entity)` is also directly callable by
|
||||
tests). Entering disables the player character's physics capsule
|
||||
(`CharacterSystem::disablePhysics`), pins its node to the resolved
|
||||
seat's offset (`VehicleComponent::seats`, or the legacy single driver
|
||||
`seatOffset` when `seats` is empty; re-pinned every frame from
|
||||
`postPhysicsUpdate()`, after the physics step syncs the vehicle node —
|
||||
see "Frame Update Order" 21a), parks the locomotion state
|
||||
machine in idle, tags the vehicle `PlayerControlledComponent`, sets
|
||||
`PlayerControllerComponent::driving` and swaps `tpsDistance`/`tpsHeight`
|
||||
for the vehicle's `cameraDistance`/`cameraHeight`. The character plays
|
||||
see "Frame Update Order" 21a), applies `seatCharacterScale` to the
|
||||
character node (restored on exit), parks the locomotion state
|
||||
machine in idle, marks the seat occupied, and — only on a driver
|
||||
seat — tags the vehicle `PlayerControlledComponent`, sets
|
||||
`PlayerControllerComponent::driving` and swaps
|
||||
`tpsDistance`/`tpsHeight` for the vehicle's
|
||||
`cameraDistance`/`cameraHeight`. The character plays
|
||||
the locomotion state machine's `PlayerControllerComponent::drivingState`
|
||||
state ("driving", mapped to the `sitting` animation in the demo's
|
||||
animation_tree.json); `idleState` is restored on exit. Entering also
|
||||
state ("driving", mapped to the `sitting` animation in the sokoban
|
||||
demo's animation_tree.json, `sitting-vehicle` in the taxi demo's);
|
||||
`idleState` is restored on exit. Entering also
|
||||
pushes a controls hint to the GameHudSystem ("W/S move, A/D steer, hold
|
||||
E to exit", plus "R/F fork up/down" when the vehicle has a fork).
|
||||
E to exit", plus "R/F fork up/down" when the vehicle has a fork;
|
||||
passengers get "Riding: hold E to exit").
|
||||
- **Driving**: `update()` (called right before `PlayerControllerSystem` in
|
||||
Game+Playing) feeds `GameInputState` into the vehicle inputs — W/S
|
||||
`inputForward`, A/D `inputRight`, Space `inputHandBrake`.
|
||||
`inputForward`, A/D `inputRight`, Space `inputHandBrake` — only when
|
||||
the player sits on a driver seat; passengers zero the inputs.
|
||||
- **Exit**: holding E while driving (a cooldown of `EXIT_MIN_SEAT_TIME`
|
||||
0.75 s after seating must pass first, so the entering tap never counts,
|
||||
then E must be held for `EXIT_HOLD_TIME` 0.5 s) picks the first
|
||||
raycast-clear spot beside (±X) or behind/ahead (±Z) of the chassis
|
||||
(horizontal clearance ray + downward ground ray; `raycastQuery` sees
|
||||
NON_MOVING only, so the vehicle never blocks its own exit), moves the
|
||||
raycast-clear spot beside the door of the seat actually occupied
|
||||
(sideways from the seat's own chassis-space row, own side first, then
|
||||
the opposite side of the same row, then behind/ahead (±Z) of the
|
||||
chassis as fallbacks; horizontal clearance ray + downward ground ray;
|
||||
`raycastQuery` sees NON_MOVING only, so the vehicle never blocks its
|
||||
own exit), moves the
|
||||
character node there, re-enables physics (the CharacterSystem diff check
|
||||
teleports the capsule to the node) and restores the on-foot camera
|
||||
settings. No clear spot → the player stays seated and a warning is
|
||||
logged.
|
||||
settings. After an exit the E key must be released once before a
|
||||
`vehicle_enter` event is accepted again (`m_eReleaseRequired`), so the
|
||||
still-held exit key cannot immediately re-seat the player; the
|
||||
E/F/I/Space pressed edges also ignore SDL key repeats
|
||||
(EditorApp::keyPressed), which previously re-fired the door actuators
|
||||
during a long hold. No clear spot → the player stays seated and a
|
||||
warning is logged.
|
||||
- **Teardown**: `resetDriving()` (no raycasts) runs from
|
||||
`EditorApp::destroySceneEntities` before `resetControllers()`, and the
|
||||
system is destroyed before `PlayerControllerSystem`/`CharacterSystem`.
|
||||
|
||||
@@ -2764,17 +2764,20 @@ bool EditorApp::keyPressed(const OgreBites::KeyboardEvent &evt)
|
||||
case 'e':
|
||||
case 'E':
|
||||
m_gameInput.e = true;
|
||||
m_gameInput.ePressed = true;
|
||||
if (!isRepeat)
|
||||
m_gameInput.ePressed = true;
|
||||
break;
|
||||
case 'f':
|
||||
case 'F':
|
||||
m_gameInput.f = true;
|
||||
m_gameInput.fPressed = true;
|
||||
if (!isRepeat)
|
||||
m_gameInput.fPressed = true;
|
||||
break;
|
||||
case 'i':
|
||||
case 'I':
|
||||
m_gameInput.i = true;
|
||||
m_gameInput.iPressed = true;
|
||||
if (!isRepeat)
|
||||
m_gameInput.iPressed = true;
|
||||
break;
|
||||
case 'r':
|
||||
case 'R':
|
||||
@@ -2785,7 +2788,8 @@ bool EditorApp::keyPressed(const OgreBites::KeyboardEvent &evt)
|
||||
break;
|
||||
case OgreBites::SDLK_SPACE:
|
||||
m_gameInput.space = true;
|
||||
m_gameInput.spacePressed = true;
|
||||
if (!isRepeat)
|
||||
m_gameInput.spacePressed = true;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -34,6 +34,24 @@ struct VehicleWheel {
|
||||
float maxHandBrakeTorque = 0.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Vehicle seat definition.
|
||||
*
|
||||
* Seats are named ("driver", "passenger", ...) so scene actions can ask
|
||||
* for a specific one; the driver flag marks the seat that gets driving
|
||||
* controls (player-controlled driving only happens there, NPC passengers
|
||||
* sit in the others).
|
||||
*/
|
||||
struct VehicleSeat {
|
||||
Ogre::String name = "driver";
|
||||
/* Seat offset in chassis space (character root, sitting pose). */
|
||||
Ogre::Vector3 offset = Ogre::Vector3(0.0f, 1.0f, 0.0f);
|
||||
bool driver = false;
|
||||
|
||||
/* Runtime: entity currently sitting here (0 = free). */
|
||||
uint64_t occupant = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Vehicle component.
|
||||
*
|
||||
@@ -52,11 +70,36 @@ struct VehicleComponent {
|
||||
/* Degrees; 0 = unlimited. */
|
||||
float maxPitchRollAngleDeg = 60.0f;
|
||||
|
||||
/* Tire friction multipliers: scale the Jolt default tire friction
|
||||
* curves (peak ~1.2) before they are combined with the ground
|
||||
* friction. 1.0 = stock asphalt tire; raise lateral for less
|
||||
* drift-prone arcade handling (sedans use ~1.8). */
|
||||
float tireFrictionLongitudinal = 1.0f;
|
||||
float tireFrictionLateral = 1.0f;
|
||||
|
||||
/* Anti-roll bar stiffness (N/m) per axle pair (Jolt
|
||||
* VehicleAntiRollBar; 0 = none). Limits body roll in hard
|
||||
* corners so a soft-sprung car does not end up on two wheels. */
|
||||
float antiRollBarStiffness = 0.0f;
|
||||
|
||||
/* Driver seat offset in chassis space (used by the
|
||||
* VehicleControllerSystem enter/exit flow to seat the player
|
||||
* character). */
|
||||
* character). Legacy single-seat shortcut: only used when seats
|
||||
* is empty, where it acts as the single driver seat. */
|
||||
Ogre::Vector3 seatOffset = Ogre::Vector3(0.0f, 1.0f, 0.0f);
|
||||
|
||||
/* All seats of the vehicle (chassis space). When empty, the
|
||||
* vehicle has one implicit driver seat at seatOffset. */
|
||||
std::vector<VehicleSeat> seats;
|
||||
|
||||
/* Uniform scale applied to the character node while seated
|
||||
* (restored on exit). The stock sit animations are proportioned
|
||||
* for a chair, not a car cabin: with a normal sedan cabin the feet
|
||||
* dangle below the rocker line and the head grazes the roof, so
|
||||
* shrinking the seated character a little (0.85) makes the pose
|
||||
* fit without cartoonish seat geometry. 1.0 = no scaling. */
|
||||
float seatCharacterScale = 1.0f;
|
||||
|
||||
/* Rear-wheel steering (like a real forklift): flips the steer sign
|
||||
* so D still turns the nose right — with the steered axle behind
|
||||
* the driver the raw wheel angle would turn the nose left. */
|
||||
|
||||
@@ -7,10 +7,11 @@ ecs.behavior_tree.register_node("luaHello", function(entity_id, params)
|
||||
end)
|
||||
|
||||
-- F10 vehicles: seat the player character on the named vehicle entity
|
||||
-- (VehicleControllerSystem listens for "vehicle_enter").
|
||||
-- (VehicleControllerSystem listens for "vehicle_enter"). "vehicle" is
|
||||
-- optional (empty = nearest vehicle to the player); "seat" picks a seat
|
||||
-- by name (empty = driver seat / first free seat).
|
||||
ecs.behavior_tree.register_node("vehicleEnter", function(entity_id, params)
|
||||
local vehicle = params.vehicle or ""
|
||||
ecs.send_event("vehicle_enter", { vehicle = vehicle })
|
||||
ecs.send_event("vehicle_enter", { vehicle = params.vehicle or "", seat = params.seat or "" })
|
||||
return "success"
|
||||
end)
|
||||
|
||||
|
||||
@@ -650,9 +650,32 @@ public:
|
||||
w->mMaxHandBrakeTorque = wd.maxHandBrakeTorque;
|
||||
w->mRadius = wd.radius;
|
||||
w->mWidth = wd.width;
|
||||
/* Scale the default tire friction curves (see the
|
||||
* WheelSettingsWV constructor) by the requested
|
||||
* multipliers; Jolt combines them with the ground
|
||||
* friction via sqrt(tire * ground). */
|
||||
for (JPH::LinearCurve::Point &p :
|
||||
w->mLongitudinalFriction.mPoints)
|
||||
p.mY *= desc.tireFrictionLongitudinal;
|
||||
for (JPH::LinearCurve::Point &p :
|
||||
w->mLateralFriction.mPoints)
|
||||
p.mY *= desc.tireFrictionLateral;
|
||||
settings.mWheels.push_back(w);
|
||||
}
|
||||
|
||||
/* One anti-roll bar per axle pair (wheels 0/1, 2/3, ...) to
|
||||
* limit body roll in corners. */
|
||||
if (desc.antiRollBarStiffness > 0.0f) {
|
||||
settings.mAntiRollBars.resize(desc.wheels.size() / 2);
|
||||
for (size_t i = 0; i + 1 < desc.wheels.size(); i += 2) {
|
||||
JPH::VehicleAntiRollBar &bar =
|
||||
settings.mAntiRollBars[i / 2];
|
||||
bar.mLeftWheel = (int)i;
|
||||
bar.mRightWheel = (int)i + 1;
|
||||
bar.mStiffness = desc.antiRollBarStiffness;
|
||||
}
|
||||
}
|
||||
|
||||
JPH::WheeledVehicleControllerSettings *controller =
|
||||
new JPH::WheeledVehicleControllerSettings;
|
||||
settings.mController = controller;
|
||||
@@ -1793,6 +1816,12 @@ public:
|
||||
physics_system.GetBodyInterface().GetAngularVelocity(
|
||||
id));
|
||||
}
|
||||
void addTorque(JPH::BodyID id, const Ogre::Vector3 &torque)
|
||||
{
|
||||
physics_system.GetBodyInterface().AddTorque(
|
||||
id, JPH::Vec3(torque.x, torque.y, torque.z),
|
||||
JPH::EActivation::Activate);
|
||||
}
|
||||
void setVelocities(JPH::BodyID id, const Ogre::Vector3 &linear,
|
||||
const Ogre::Vector3 &angular)
|
||||
{
|
||||
@@ -2219,6 +2248,11 @@ Ogre::Vector3 JoltPhysicsWrapper::getAngularVelocity(JPH::BodyID id)
|
||||
{
|
||||
return phys->getAngularVelocity(id);
|
||||
}
|
||||
void JoltPhysicsWrapper::addTorque(JPH::BodyID id,
|
||||
const Ogre::Vector3 &torque)
|
||||
{
|
||||
phys->addTorque(id, torque);
|
||||
}
|
||||
void JoltPhysicsWrapper::setVelocities(JPH::BodyID id,
|
||||
const Ogre::Vector3 &linear,
|
||||
const Ogre::Vector3 &angular)
|
||||
|
||||
@@ -232,6 +232,7 @@ public:
|
||||
bool activate = true);
|
||||
Ogre::Vector3 getLinearVelocity(JPH::BodyID id);
|
||||
Ogre::Vector3 getAngularVelocity(JPH::BodyID id);
|
||||
void addTorque(JPH::BodyID id, const Ogre::Vector3 &torque);
|
||||
void setVelocities(JPH::BodyID id, const Ogre::Vector3 &linear,
|
||||
const Ogre::Vector3 &angular);
|
||||
float getFriction(JPH::BodyID id);
|
||||
@@ -307,6 +308,12 @@ public:
|
||||
float maxTorque = 400.0f;
|
||||
/* Degrees; 0 = unlimited. */
|
||||
float maxPitchRollAngleDeg = 60.0f;
|
||||
/* Multipliers for the default tire friction curves
|
||||
* (1.0 = stock asphalt tire). */
|
||||
float tireFrictionLongitudinal = 1.0f;
|
||||
float tireFrictionLateral = 1.0f;
|
||||
/* Anti-roll bar stiffness (N/m) per axle pair; 0 = none. */
|
||||
float antiRollBarStiffness = 0.0f;
|
||||
};
|
||||
|
||||
/* Create/destroy a vehicle on an existing (already added) dynamic
|
||||
|
||||
@@ -549,6 +549,22 @@ void AnimationTreeSystem::update(float deltaTime)
|
||||
}
|
||||
}
|
||||
|
||||
/* Disable animations that were not evaluated this frame.
|
||||
* When a fading-out animation's weight drops to zero in a
|
||||
* single step (large deltaTime * fadeSpeed), evaluateNode
|
||||
* skips it entirely and it never appears in ctx.animData;
|
||||
* without this, its Ogre::AnimationState would stay enabled
|
||||
* forever at its last non-zero weight with frozen time. */
|
||||
for (auto &pair : state.animations) {
|
||||
Ogre::AnimationState *as = pair.second.ogreAnimState;
|
||||
if (!as || !as->getEnabled())
|
||||
continue;
|
||||
if (ctx.animData.find(pair.first) == ctx.animData.end()) {
|
||||
as->setEnabled(false);
|
||||
as->setWeight(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
if (at.useRootMotion && sceneNode) {
|
||||
/* Smooth root-motion displacement with EMA to
|
||||
* filter high-frequency hip oscillation within
|
||||
|
||||
@@ -1669,9 +1669,30 @@ nlohmann::json SceneSerializer::serializeVehicle(flecs::entity entity)
|
||||
|
||||
json["maxTorque"] = vehicle.maxTorque;
|
||||
json["maxPitchRollAngleDeg"] = vehicle.maxPitchRollAngleDeg;
|
||||
if (vehicle.tireFrictionLongitudinal != 1.0f)
|
||||
json["tireFrictionLongitudinal"] =
|
||||
vehicle.tireFrictionLongitudinal;
|
||||
if (vehicle.tireFrictionLateral != 1.0f)
|
||||
json["tireFrictionLateral"] = vehicle.tireFrictionLateral;
|
||||
if (vehicle.antiRollBarStiffness != 0.0f)
|
||||
json["antiRollBarStiffness"] = vehicle.antiRollBarStiffness;
|
||||
json["seatOffset"] = { { "x", vehicle.seatOffset.x },
|
||||
{ "y", vehicle.seatOffset.y },
|
||||
{ "z", vehicle.seatOffset.z } };
|
||||
nlohmann::json seats = nlohmann::json::array();
|
||||
for (const VehicleSeat &s : vehicle.seats) {
|
||||
nlohmann::json sj;
|
||||
sj["name"] = s.name;
|
||||
sj["offset"] = { { "x", s.offset.x },
|
||||
{ "y", s.offset.y },
|
||||
{ "z", s.offset.z } };
|
||||
sj["driver"] = s.driver;
|
||||
seats.push_back(sj);
|
||||
}
|
||||
if (!vehicle.seats.empty())
|
||||
json["seats"] = seats;
|
||||
if (vehicle.seatCharacterScale != 1.0f)
|
||||
json["seatCharacterScale"] = vehicle.seatCharacterScale;
|
||||
json["wheelMeshName"] = vehicle.wheelMeshName;
|
||||
json["steeringWheelMeshName"] = vehicle.steeringWheelMeshName;
|
||||
json["steeringWheelOffset"] = {
|
||||
@@ -1721,12 +1742,33 @@ void SceneSerializer::deserializeVehicle(flecs::entity entity,
|
||||
vehicle.maxTorque = json.value("maxTorque", 400.0f);
|
||||
vehicle.maxPitchRollAngleDeg =
|
||||
json.value("maxPitchRollAngleDeg", 60.0f);
|
||||
vehicle.tireFrictionLongitudinal =
|
||||
json.value("tireFrictionLongitudinal", 1.0f);
|
||||
vehicle.tireFrictionLateral =
|
||||
json.value("tireFrictionLateral", 1.0f);
|
||||
vehicle.antiRollBarStiffness =
|
||||
json.value("antiRollBarStiffness", 0.0f);
|
||||
if (json.contains("seatOffset")) {
|
||||
auto &so = json["seatOffset"];
|
||||
vehicle.seatOffset = Ogre::Vector3(so.value("x", 0.0f),
|
||||
so.value("y", 1.0f),
|
||||
so.value("z", 0.0f));
|
||||
}
|
||||
if (json.contains("seats")) {
|
||||
for (const auto &sj : json["seats"]) {
|
||||
VehicleSeat s;
|
||||
s.name = sj.value("name", "driver");
|
||||
if (sj.contains("offset")) {
|
||||
auto &o = sj["offset"];
|
||||
s.offset = Ogre::Vector3(o.value("x", 0.0f),
|
||||
o.value("y", 1.0f),
|
||||
o.value("z", 0.0f));
|
||||
}
|
||||
s.driver = sj.value("driver", false);
|
||||
vehicle.seats.push_back(s);
|
||||
}
|
||||
}
|
||||
vehicle.seatCharacterScale = json.value("seatCharacterScale", 1.0f);
|
||||
vehicle.wheelMeshName = json.value("wheelMeshName", "");
|
||||
vehicle.steeringWheelMeshName =
|
||||
json.value("steeringWheelMeshName", "");
|
||||
|
||||
@@ -67,40 +67,138 @@ flecs::entity VehicleControllerSystem::findPlayerController()
|
||||
return result;
|
||||
}
|
||||
|
||||
flecs::entity VehicleControllerSystem::findNearestVehicle(
|
||||
const Ogre::Vector3 &pos, float maxDist) const
|
||||
{
|
||||
flecs::entity result = flecs::entity::null();
|
||||
float best = maxDist;
|
||||
m_world.query<VehicleComponent, TransformComponent>().each(
|
||||
[&](flecs::entity e, VehicleComponent &, TransformComponent &t) {
|
||||
if (!t.node)
|
||||
return;
|
||||
float d = t.node->_getDerivedPosition().distance(pos);
|
||||
if (d < best) {
|
||||
best = d;
|
||||
result = e;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void VehicleControllerSystem::onVehicleEnterEvent(
|
||||
const Ogre::String &eventName, const editScene::EventParams ¶ms)
|
||||
{
|
||||
(void)eventName;
|
||||
if (m_driving)
|
||||
return;
|
||||
/* After an exit, E must have been released once before it can
|
||||
* seat the player again (the exit is a hold, so the key is still
|
||||
* down when the exit spot lands next to a door actuator). */
|
||||
if (m_eReleaseRequired)
|
||||
return;
|
||||
|
||||
Ogre::String vehicleName = params.getString("vehicle", "");
|
||||
if (vehicleName.empty()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[VehicleControllerSystem] vehicle_enter event without "
|
||||
"a 'vehicle' param, ignored");
|
||||
return;
|
||||
}
|
||||
Ogre::String seatName = params.getString("seat", "");
|
||||
|
||||
flecs::entity vehicle = flecs::entity::null();
|
||||
m_world.query<EntityNameComponent>().each(
|
||||
[&](flecs::entity e, EntityNameComponent &name) {
|
||||
if (vehicle.is_alive())
|
||||
return;
|
||||
if (name.name == vehicleName)
|
||||
vehicle = e;
|
||||
});
|
||||
if (!vehicle.is_alive()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[VehicleControllerSystem] vehicle_enter: no entity "
|
||||
"named '" +
|
||||
vehicleName + "'");
|
||||
return;
|
||||
if (vehicleName.empty()) {
|
||||
/* No vehicle named: take the nearest one to the player
|
||||
* character (per-door seat actuators are generic and rely
|
||||
* on this). */
|
||||
flecs::entity character =
|
||||
m_editorApp->getPlayerCharacterEntity();
|
||||
if (character.is_alive() &&
|
||||
character.has<TransformComponent>() &&
|
||||
character.get<TransformComponent>().node) {
|
||||
vehicle = findNearestVehicle(
|
||||
character.get<TransformComponent>()
|
||||
.node->_getDerivedPosition(),
|
||||
5.0f);
|
||||
}
|
||||
if (!vehicle.is_alive()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[VehicleControllerSystem] vehicle_enter: no "
|
||||
"'vehicle' param and no vehicle near the player");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
m_world.query<EntityNameComponent>().each(
|
||||
[&](flecs::entity e, EntityNameComponent &name) {
|
||||
if (vehicle.is_alive())
|
||||
return;
|
||||
if (name.name == vehicleName)
|
||||
vehicle = e;
|
||||
});
|
||||
if (!vehicle.is_alive()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[VehicleControllerSystem] vehicle_enter: no entity "
|
||||
"named '" +
|
||||
vehicleName + "'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
enterVehicle(vehicle);
|
||||
enterVehicle(vehicle, seatName);
|
||||
}
|
||||
|
||||
int VehicleControllerSystem::seatCount(const VehicleComponent &vc) const
|
||||
{
|
||||
return vc.seats.empty() ? 1 : (int)vc.seats.size();
|
||||
}
|
||||
|
||||
Ogre::Vector3 VehicleControllerSystem::seatOffsetAt(
|
||||
const VehicleComponent &vc, int index) const
|
||||
{
|
||||
if (vc.seats.empty())
|
||||
return vc.seatOffset;
|
||||
return vc.seats[index].offset;
|
||||
}
|
||||
|
||||
bool VehicleControllerSystem::seatIsDriverAt(const VehicleComponent &vc,
|
||||
int index) const
|
||||
{
|
||||
if (vc.seats.empty())
|
||||
return true; /* the implicit seatOffset seat is the driver */
|
||||
return vc.seats[index].driver;
|
||||
}
|
||||
|
||||
bool VehicleControllerSystem::seatIsFreeAt(const VehicleComponent &vc,
|
||||
int index) const
|
||||
{
|
||||
if (vc.seats.empty())
|
||||
return true; /* single player, implicit seat: untracked */
|
||||
return vc.seats[index].occupant == 0;
|
||||
}
|
||||
|
||||
int VehicleControllerSystem::resolveSeat(const VehicleComponent &vc,
|
||||
const Ogre::String &requested) const
|
||||
{
|
||||
int count = seatCount(vc);
|
||||
if (!requested.empty()) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
Ogre::String name = vc.seats.empty() ?
|
||||
"driver" :
|
||||
vc.seats[i].name;
|
||||
if (name == requested && seatIsFreeAt(vc, i))
|
||||
return i;
|
||||
}
|
||||
}
|
||||
/* Fall back to the driver seat, then to any free seat. */
|
||||
for (int i = 0; i < count; i++)
|
||||
if (seatIsDriverAt(vc, i) && seatIsFreeAt(vc, i))
|
||||
return i;
|
||||
for (int i = 0; i < count; i++)
|
||||
if (seatIsFreeAt(vc, i))
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool VehicleControllerSystem::enterVehicle(flecs::entity vehicle)
|
||||
{
|
||||
return enterVehicle(vehicle, "");
|
||||
}
|
||||
|
||||
bool VehicleControllerSystem::enterVehicle(flecs::entity vehicle,
|
||||
const Ogre::String &seatName)
|
||||
{
|
||||
if (m_driving)
|
||||
return false;
|
||||
@@ -126,24 +224,59 @@ bool VehicleControllerSystem::enterVehicle(flecs::entity vehicle)
|
||||
return false;
|
||||
auto &pc = controller.get_mut<PlayerControllerComponent>();
|
||||
|
||||
/* Pick the seat: the requested one when free, else the driver
|
||||
* seat, else any free seat. */
|
||||
const VehicleComponent &vcc = vehicle.get<VehicleComponent>();
|
||||
int seatIndex = resolveSeat(vcc, seatName);
|
||||
if (seatIndex < 0) {
|
||||
GameHudSystem *hud = m_editorApp->getGameHudSystem();
|
||||
if (hud)
|
||||
hud->pushMessage("No free seat in this vehicle",
|
||||
4.0f);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_savedTpsDistance = pc.tpsDistance;
|
||||
m_savedTpsHeight = pc.tpsHeight;
|
||||
pc.driving = true;
|
||||
pc.tpsDistance = vehicle.get<VehicleComponent>().cameraDistance;
|
||||
pc.tpsHeight = vehicle.get<VehicleComponent>().cameraHeight;
|
||||
pc.tpsDistance = vcc.cameraDistance;
|
||||
pc.tpsHeight = vcc.cameraHeight;
|
||||
|
||||
CharacterSystem *cs = m_editorApp->getCharacterSystem();
|
||||
if (cs)
|
||||
cs->disablePhysics(character);
|
||||
|
||||
/* Switch to the driving animation state (currently mapped to the
|
||||
* "sitting" animation in the locomotion state machine). */
|
||||
/* Switch to the driving animation state (the animation it maps
|
||||
* to comes from the scene's animation tree; demo-taxi maps it to
|
||||
* "sitting-vehicle"). */
|
||||
AnimationTreeSystem *ats = m_editorApp->getAnimationTreeSystem();
|
||||
if (ats)
|
||||
ats->setState(character, pc.locomotionStateMachine,
|
||||
pc.drivingState);
|
||||
|
||||
vehicle.add<PlayerControlledComponent>();
|
||||
m_seatIndex = seatIndex;
|
||||
m_seatIsDriver = seatIsDriverAt(vcc, seatIndex);
|
||||
if (!vcc.seats.empty()) {
|
||||
auto &vcm = vehicle.get_mut<VehicleComponent>();
|
||||
vcm.seats[seatIndex].occupant = character.id();
|
||||
}
|
||||
|
||||
/* Fit the character into the cabin (seatCharacterScale; restored
|
||||
* by restoreOnFoot). */
|
||||
{
|
||||
auto &ctm = character.get_mut<TransformComponent>();
|
||||
if (ctm.node) {
|
||||
m_savedCharScale = ctm.node->getScale();
|
||||
if (vcc.seatCharacterScale != 1.0f)
|
||||
ctm.node->setScale(Ogre::Vector3(
|
||||
vcc.seatCharacterScale));
|
||||
}
|
||||
}
|
||||
|
||||
/* Only the driver seat marks the vehicle player-controlled and
|
||||
* gets the driving inputs; passengers just sit. */
|
||||
if (m_seatIsDriver)
|
||||
vehicle.add<PlayerControlledComponent>();
|
||||
|
||||
m_driving = true;
|
||||
m_vehicleId = vehicle.id();
|
||||
@@ -158,11 +291,13 @@ bool VehicleControllerSystem::enterVehicle(flecs::entity vehicle)
|
||||
* actually has a fork). */
|
||||
GameHudSystem *hud = m_editorApp->getGameHudSystem();
|
||||
if (hud) {
|
||||
Ogre::String hint =
|
||||
"Driving: W/S move, A/D steer, hold E to exit";
|
||||
if (!vehicle.get<VehicleComponent>().forkMeshName.empty())
|
||||
hint = "Driving: W/S move, A/D steer, R/F fork "
|
||||
"up/down, hold E to exit";
|
||||
Ogre::String hint = "Riding: hold E to exit";
|
||||
if (m_seatIsDriver) {
|
||||
hint = "Driving: W/S move, A/D steer, hold E to exit";
|
||||
if (!vcc.forkMeshName.empty())
|
||||
hint = "Driving: W/S move, A/D steer, R/F fork "
|
||||
"up/down, hold E to exit";
|
||||
}
|
||||
hud->pushMessage(hint, 10.0f);
|
||||
}
|
||||
|
||||
@@ -184,7 +319,7 @@ void VehicleControllerSystem::pinCharacterToSeat(flecs::entity vehicle,
|
||||
* uses setPosition), so derived == world here; vehicle scale is
|
||||
* 1 in practice. */
|
||||
Ogre::Vector3 seat = vt.node->_getDerivedOrientation() *
|
||||
vc.seatOffset +
|
||||
seatOffsetAt(vc, m_seatIndex) +
|
||||
vt.node->_getDerivedPosition();
|
||||
ct.node->setPosition(seat);
|
||||
ct.node->setOrientation(vt.node->_getDerivedOrientation());
|
||||
@@ -193,6 +328,9 @@ void VehicleControllerSystem::pinCharacterToSeat(flecs::entity vehicle,
|
||||
|
||||
void VehicleControllerSystem::update(float deltaTime)
|
||||
{
|
||||
if (m_eReleaseRequired && !m_editorApp->getGameInputState().e)
|
||||
m_eReleaseRequired = false;
|
||||
|
||||
if (!m_driving)
|
||||
return;
|
||||
|
||||
@@ -209,6 +347,8 @@ void VehicleControllerSystem::update(float deltaTime)
|
||||
m_vehicleId = 0;
|
||||
m_characterId = 0;
|
||||
m_controllerId = 0;
|
||||
m_seatIndex = -1;
|
||||
m_seatIsDriver = false;
|
||||
return;
|
||||
}
|
||||
if (!character.is_alive()) {
|
||||
@@ -217,17 +357,28 @@ void VehicleControllerSystem::update(float deltaTime)
|
||||
m_vehicleId = 0;
|
||||
m_characterId = 0;
|
||||
m_controllerId = 0;
|
||||
m_seatIndex = -1;
|
||||
m_seatIsDriver = false;
|
||||
return;
|
||||
}
|
||||
|
||||
GameInputState &input = m_editorApp->getGameInputState();
|
||||
auto &vc = vehicle.get_mut<VehicleComponent>();
|
||||
vc.inputForward = input.w ? 1.0f : (input.s ? -1.0f : 0.0f);
|
||||
vc.inputRight = input.d ? 1.0f : (input.a ? -1.0f : 0.0f);
|
||||
vc.inputBrake = 0.0f;
|
||||
vc.inputHandBrake = input.space ? 1.0f : 0.0f;
|
||||
/* Fork lift: R raises, F lowers (hold). */
|
||||
vc.inputFork = input.r ? 1.0f : (input.f ? -1.0f : 0.0f);
|
||||
if (m_seatIsDriver) {
|
||||
vc.inputForward = input.w ? 1.0f : (input.s ? -1.0f : 0.0f);
|
||||
vc.inputRight = input.d ? 1.0f : (input.a ? -1.0f : 0.0f);
|
||||
vc.inputBrake = 0.0f;
|
||||
vc.inputHandBrake = input.space ? 1.0f : 0.0f;
|
||||
/* Fork lift: R raises, F lowers (hold). */
|
||||
vc.inputFork = input.r ? 1.0f : (input.f ? -1.0f : 0.0f);
|
||||
} else {
|
||||
/* Passenger seat: no driving inputs reach the vehicle. */
|
||||
vc.inputForward = 0.0f;
|
||||
vc.inputRight = 0.0f;
|
||||
vc.inputBrake = 0.0f;
|
||||
vc.inputHandBrake = 0.0f;
|
||||
vc.inputFork = 0.0f;
|
||||
}
|
||||
|
||||
/* The seat pin runs in postPhysicsUpdate() (after the physics
|
||||
* step syncs the vehicle node) so the driver and the seat are
|
||||
@@ -261,6 +412,7 @@ void VehicleControllerSystem::postPhysicsUpdate()
|
||||
}
|
||||
|
||||
bool VehicleControllerSystem::findExitSpot(flecs::entity vehicle,
|
||||
const Ogre::Vector3 &seatOffset,
|
||||
Ogre::Vector3 &out) const
|
||||
{
|
||||
const TransformComponent &vt = vehicle.get<TransformComponent>();
|
||||
@@ -272,16 +424,22 @@ bool VehicleControllerSystem::findExitSpot(flecs::entity vehicle,
|
||||
Ogre::Vector3 right = vor * Ogre::Vector3::UNIT_X;
|
||||
Ogre::Vector3 fwd = vor * Ogre::Vector3::UNIT_Z;
|
||||
|
||||
/* Exit through the door of the seat actually occupied: sideways
|
||||
* from the seat's own row (chassis +X is the left/driver side),
|
||||
* then the opposite side of the same row, then behind/ahead of
|
||||
* the vehicle as fallbacks. */
|
||||
float side = seatOffset.x >= 0.0f ? 1.0f : -1.0f;
|
||||
Ogre::Vector3 seatWorld = vor * seatOffset;
|
||||
const Ogre::Vector3 candidates[4] = {
|
||||
vpos + right * EXIT_SIDE_OFFSET,
|
||||
vpos - right * EXIT_SIDE_OFFSET,
|
||||
seatWorld + right * (side * EXIT_SIDE_OFFSET),
|
||||
seatWorld - right * (side * EXIT_SIDE_OFFSET),
|
||||
vpos - fwd * EXIT_END_OFFSET,
|
||||
vpos + fwd * EXIT_END_OFFSET,
|
||||
};
|
||||
|
||||
JoltPhysicsWrapper *physics = JoltPhysicsWrapper::getSingletonPtr();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Ogre::Vector3 spot = candidates[i];
|
||||
Ogre::Vector3 spot = vpos + candidates[i];
|
||||
if (!physics) {
|
||||
out = spot;
|
||||
return true;
|
||||
@@ -317,6 +475,12 @@ void VehicleControllerSystem::restoreOnFoot()
|
||||
CharacterSystem *cs = m_editorApp->getCharacterSystem();
|
||||
if (cs)
|
||||
cs->enablePhysics(character);
|
||||
if (character.has<TransformComponent>()) {
|
||||
const TransformComponent &ct =
|
||||
character.get<TransformComponent>();
|
||||
if (ct.node)
|
||||
ct.node->setScale(m_savedCharScale);
|
||||
}
|
||||
}
|
||||
|
||||
if (controller.is_alive() &&
|
||||
@@ -350,8 +514,14 @@ void VehicleControllerSystem::exitVehicle()
|
||||
return;
|
||||
}
|
||||
|
||||
/* The exit spot depends on the seat actually occupied, so grab the
|
||||
* seat offset before the state below is cleared. */
|
||||
int seatIndex = m_seatIndex >= 0 ? m_seatIndex : 0;
|
||||
Ogre::Vector3 seatOffset =
|
||||
seatOffsetAt(vehicle.get<VehicleComponent>(), seatIndex);
|
||||
|
||||
Ogre::Vector3 spot;
|
||||
if (!findExitSpot(vehicle, spot)) {
|
||||
if (!findExitSpot(vehicle, seatOffset, spot)) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[VehicleControllerSystem] no clear exit spot, "
|
||||
"staying seated");
|
||||
@@ -366,13 +536,17 @@ void VehicleControllerSystem::exitVehicle()
|
||||
ct.node->setPosition(spot);
|
||||
ct.position = spot;
|
||||
|
||||
/* Zero the driver inputs so the vehicle rolls to a stop. */
|
||||
/* Zero the driver inputs so the vehicle rolls to a stop, and free
|
||||
* the seat. */
|
||||
auto &vc = vehicle.get_mut<VehicleComponent>();
|
||||
vc.inputForward = 0.0f;
|
||||
vc.inputRight = 0.0f;
|
||||
vc.inputBrake = 0.0f;
|
||||
vc.inputHandBrake = 0.0f;
|
||||
vc.inputFork = 0.0f;
|
||||
if (!vc.seats.empty() && m_seatIndex >= 0 &&
|
||||
m_seatIndex < (int)vc.seats.size())
|
||||
vc.seats[m_seatIndex].occupant = 0;
|
||||
|
||||
vehicle.remove<PlayerControlledComponent>();
|
||||
restoreOnFoot();
|
||||
@@ -381,6 +555,9 @@ void VehicleControllerSystem::exitVehicle()
|
||||
m_vehicleId = 0;
|
||||
m_characterId = 0;
|
||||
m_controllerId = 0;
|
||||
m_seatIndex = -1;
|
||||
m_seatIsDriver = false;
|
||||
m_eReleaseRequired = true;
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[VehicleControllerSystem] exited vehicle");
|
||||
@@ -400,6 +577,9 @@ void VehicleControllerSystem::resetDriving()
|
||||
vc.inputBrake = 0.0f;
|
||||
vc.inputHandBrake = 0.0f;
|
||||
vc.inputFork = 0.0f;
|
||||
if (!vc.seats.empty() && m_seatIndex >= 0 &&
|
||||
m_seatIndex < (int)vc.seats.size())
|
||||
vc.seats[m_seatIndex].occupant = 0;
|
||||
}
|
||||
vehicle.remove<PlayerControlledComponent>();
|
||||
}
|
||||
@@ -410,4 +590,6 @@ void VehicleControllerSystem::resetDriving()
|
||||
m_vehicleId = 0;
|
||||
m_characterId = 0;
|
||||
m_controllerId = 0;
|
||||
m_seatIndex = -1;
|
||||
m_seatIsDriver = false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "EventBus.hpp"
|
||||
#include "../components/Vehicle.hpp"
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
|
||||
@@ -11,25 +12,33 @@ class EditorApp;
|
||||
/**
|
||||
* Vehicle enter/exit + drive input (game mode only, F10).
|
||||
*
|
||||
* Entering: a scene action (typically an actuator on the vehicle entity)
|
||||
* runs a luaTask that sends the "vehicle_enter" event with a "vehicle"
|
||||
* param naming the vehicle entity; this system listens for that event and
|
||||
* seats the player character: the character's physics capsule is disabled
|
||||
* Entering: a scene action (typically an actuator on the vehicle entity,
|
||||
* or on one of its door children) runs a luaTask that sends the
|
||||
* "vehicle_enter" event with an optional "vehicle" param naming the
|
||||
* vehicle entity (empty = nearest vehicle to the player character) and
|
||||
* an optional "seat" param naming the seat (empty = driver seat, or the
|
||||
* first free seat); this system listens for that event and seats the
|
||||
* player character: the character's physics capsule is disabled
|
||||
* (CharacterSystem::disablePhysics), its scene node is pinned to the
|
||||
* vehicle's VehicleComponent::seatOffset every frame (after the physics
|
||||
* step, so the seat never jitters under the driver — see
|
||||
* postPhysicsUpdate), and the player controller's TPS boom is swapped for
|
||||
* the vehicle's cameraDistance / cameraHeight (restored on exit).
|
||||
* seat offset (VehicleComponent::seats, or the legacy single driver
|
||||
* seatOffset when seats is empty) every frame (after the physics step,
|
||||
* so the seat never jitters under the driver — see postPhysicsUpdate),
|
||||
* and the player controller's TPS boom is swapped for the vehicle's
|
||||
* cameraDistance / cameraHeight (restored on exit).
|
||||
* PlayerControllerComponent::driving gates off locomotion and actuator
|
||||
* prompts while seated.
|
||||
*
|
||||
* Driving: W/S feed VehicleComponent::inputForward, A/D inputRight,
|
||||
* Space inputHandBrake; VehicleSystem consumes them in the physics step.
|
||||
* Only the driver seat (VehicleSeat::driver, or the implicit seatOffset
|
||||
* seat) gets these inputs and marks the vehicle PlayerControlledComponent;
|
||||
* passengers sit without any control over the vehicle.
|
||||
*
|
||||
* Exit: holding E (a longer press than entering: a short cooldown after
|
||||
* seating must pass first, then E must be held for a moment) places the
|
||||
* character on the first raycast-clear spot beside
|
||||
* (then behind/ahead of) the vehicle, re-enables its physics and restores
|
||||
* character on the first raycast-clear spot beside the door of the seat
|
||||
* actually occupied (then the opposite side of the same row, then
|
||||
* behind/ahead of the vehicle), re-enables its physics and restores
|
||||
* the on-foot camera. When no spot is clear the player stays seated and
|
||||
* a warning is logged.
|
||||
*
|
||||
@@ -61,9 +70,14 @@ public:
|
||||
}
|
||||
flecs::entity getDrivenVehicle() const;
|
||||
|
||||
/** Seat the player character on the vehicle. Returns false when
|
||||
* already driving or the entity is not a usable vehicle. */
|
||||
/** Seat the player character on the vehicle (driver seat by
|
||||
* default). Returns false when already driving or the entity is
|
||||
* not a usable vehicle. */
|
||||
bool enterVehicle(flecs::entity vehicle);
|
||||
/** Same, but asks for the named seat first (falls back to the
|
||||
* driver seat, then to any free seat). */
|
||||
bool enterVehicle(flecs::entity vehicle,
|
||||
const Ogre::String &seatName);
|
||||
/** Place the character next to the vehicle and restore on-foot
|
||||
* controls. No-op when not driving. */
|
||||
void exitVehicle();
|
||||
@@ -75,9 +89,20 @@ private:
|
||||
void onVehicleEnterEvent(const Ogre::String &eventName,
|
||||
const editScene::EventParams ¶ms);
|
||||
flecs::entity findPlayerController();
|
||||
flecs::entity findNearestVehicle(const Ogre::Vector3 &pos,
|
||||
float maxDist) const;
|
||||
int seatCount(const VehicleComponent &vc) const;
|
||||
Ogre::Vector3 seatOffsetAt(const VehicleComponent &vc,
|
||||
int index) const;
|
||||
bool seatIsDriverAt(const VehicleComponent &vc, int index) const;
|
||||
bool seatIsFreeAt(const VehicleComponent &vc, int index) const;
|
||||
int resolveSeat(const VehicleComponent &vc,
|
||||
const Ogre::String &requested) const;
|
||||
void pinCharacterToSeat(flecs::entity vehicle,
|
||||
flecs::entity character);
|
||||
bool findExitSpot(flecs::entity vehicle, Ogre::Vector3 &out) const;
|
||||
bool findExitSpot(flecs::entity vehicle,
|
||||
const Ogre::Vector3 &seatOffset,
|
||||
Ogre::Vector3 &out) const;
|
||||
void restoreOnFoot();
|
||||
|
||||
flecs::world &m_world;
|
||||
@@ -88,12 +113,22 @@ private:
|
||||
flecs::entity_t m_vehicleId = 0;
|
||||
flecs::entity_t m_characterId = 0;
|
||||
flecs::entity_t m_controllerId = 0;
|
||||
/* Resolved seat: index into VehicleComponent::seats (0 for the
|
||||
* implicit seatOffset seat) and whether it has driving controls. */
|
||||
int m_seatIndex = -1;
|
||||
bool m_seatIsDriver = false;
|
||||
/* Stashed on-foot camera settings, restored on exit. */
|
||||
float m_savedTpsDistance = 3.0f;
|
||||
float m_savedTpsHeight = 2.0f;
|
||||
/* Stashed character node scale (seatCharacterScale is applied
|
||||
* while seated), restored on exit. */
|
||||
Ogre::Vector3 m_savedCharScale = Ogre::Vector3::UNIT_SCALE;
|
||||
/* Exit debounce: time seated and current E hold duration. */
|
||||
float m_seatTime = 0.0f;
|
||||
float m_exitHold = 0.0f;
|
||||
/* Set on exit: the E key (still held from the exit hold) must be
|
||||
* released once before a vehicle_enter event is accepted again. */
|
||||
bool m_eReleaseRequired = false;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_VEHICLECONTROLLERSYSTEM_HPP
|
||||
|
||||
@@ -62,6 +62,9 @@ void VehicleSystem::createConstraint(flecs::entity entity,
|
||||
JoltPhysicsWrapper::VehicleDesc desc;
|
||||
desc.maxTorque = vehicle.maxTorque;
|
||||
desc.maxPitchRollAngleDeg = vehicle.maxPitchRollAngleDeg;
|
||||
desc.tireFrictionLongitudinal = vehicle.tireFrictionLongitudinal;
|
||||
desc.tireFrictionLateral = vehicle.tireFrictionLateral;
|
||||
desc.antiRollBarStiffness = vehicle.antiRollBarStiffness;
|
||||
desc.wheels.reserve(vehicle.wheels.size());
|
||||
for (const VehicleWheel &vw : vehicle.wheels) {
|
||||
JoltPhysicsWrapper::VehicleWheelDesc wd;
|
||||
@@ -317,7 +320,7 @@ void VehicleSystem::prePhysicsUpdate(float deltaTime)
|
||||
|
||||
m_query.each([&](flecs::entity entity, VehicleComponent &vehicle,
|
||||
RigidBodyComponent &rigidBody,
|
||||
TransformComponent &) {
|
||||
TransformComponent &transform) {
|
||||
/* The chassis body went away (disabled, rebuilt, scene
|
||||
* teardown): the constraint references the old body, drop
|
||||
* it. */
|
||||
@@ -390,6 +393,49 @@ void VehicleSystem::prePhysicsUpdate(float deltaTime)
|
||||
float blend = std::min(1.0f, deltaTime * 8.0f);
|
||||
steer += (steerTarget - steer) * blend;
|
||||
|
||||
/* Anti-two-wheel righting: a high-grip car can end up
|
||||
* balanced on two wheels at ~45 deg after a hard turn and
|
||||
* just stay there. When the chassis tilt is past ~35 deg
|
||||
* but short of fully rolled (lying on the side/roof is
|
||||
* fine and settles on its own), apply a torque about the
|
||||
* (chassis up x world up) axis to put it back on four
|
||||
* wheels. Above ~5 deg of tilt, also damp the angular
|
||||
* velocity along the same axis so the car does not rock
|
||||
* on its soft suspension for seconds afterwards. The
|
||||
* thresholds are above any sane road slope, so parked
|
||||
* cars on hills are untouched. */
|
||||
if (transform.node && rigidBody.bodyCreated) {
|
||||
Ogre::Vector3 up =
|
||||
transform.node->_getDerivedOrientation() *
|
||||
Ogre::Vector3::UNIT_Y;
|
||||
const float cosDamp = 0.9962f; /* 5 deg */
|
||||
const float cosMin = 0.8192f; /* 35 deg */
|
||||
const float cosMax = 0.1736f; /* 80 deg */
|
||||
if (up.y < cosDamp && up.y > cosMax) {
|
||||
Ogre::Vector3 axis = up.crossProduct(
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
float sinTilt = axis.length();
|
||||
axis /= sinTilt;
|
||||
Ogre::Vector3 angVel =
|
||||
m_physics->getAngularVelocity(
|
||||
rigidBody.bodyID);
|
||||
float mass =
|
||||
std::max(1.0f, rigidBody.mass);
|
||||
/* Damping along the righting axis
|
||||
* (calms rocking in both directions). */
|
||||
Ogre::Vector3 torque =
|
||||
axis * (-1.0f * mass *
|
||||
axis.dotProduct(angVel));
|
||||
/* Righting torque proportional to
|
||||
* sin(tilt), only past ~35 deg. */
|
||||
if (up.y < cosMin)
|
||||
torque += axis * (0.6f * mass *
|
||||
9.81f * sinTilt);
|
||||
m_physics->addTorque(rigidBody.bodyID,
|
||||
torque);
|
||||
}
|
||||
}
|
||||
|
||||
updateFork(entity, vehicle, rigidBody, deltaTime);
|
||||
});
|
||||
}
|
||||
@@ -409,6 +455,15 @@ void VehicleSystem::postPhysicsUpdate()
|
||||
|
||||
Ogre::Quaternion invChassis =
|
||||
transform.node->_getDerivedOrientation().Inverse();
|
||||
/* Jolt reports every wheel with the same axes (mirrored
|
||||
* position, same axle direction), so a single wheel mesh has
|
||||
* its outer face pointing inward on the right side (chassis
|
||||
* -X). Flip right-side wheels 180 degrees about the local
|
||||
* forward axis: the outer face points outward again and the
|
||||
* spin about the axle is visually unchanged, so one shared
|
||||
* wheel mesh works for all four corners. */
|
||||
const Ogre::Quaternion rightSideFlip(
|
||||
Ogre::Degree(180.0f), Ogre::Vector3::UNIT_Z);
|
||||
for (size_t i = 0; i < it->second.nodes.size(); i++) {
|
||||
Ogre::Vector3 pos;
|
||||
Ogre::Quaternion ori;
|
||||
@@ -417,7 +472,12 @@ void VehicleSystem::postPhysicsUpdate()
|
||||
Ogre::SceneNode *node = it->second.nodes[i];
|
||||
node->setPosition(transform.node->convertWorldToLocalPosition(
|
||||
pos));
|
||||
node->setOrientation(invChassis * ori);
|
||||
bool rightSide =
|
||||
i < vehicle.wheels.size() &&
|
||||
vehicle.wheels[i].position.x < 0.0f;
|
||||
node->setOrientation(invChassis * ori *
|
||||
(rightSide ? rightSideFlip :
|
||||
Ogre::Quaternion::IDENTITY));
|
||||
}
|
||||
|
||||
/* Fork visual: follow the fork height. */
|
||||
|
||||
@@ -17,9 +17,79 @@ bool VehicleEditor::renderComponent(flecs::entity entity,
|
||||
&vehicle.maxPitchRollAngleDeg, 0.5f,
|
||||
0.0f, 90.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Tire Friction (long.)",
|
||||
&vehicle.tireFrictionLongitudinal,
|
||||
0.05f, 0.0f, 5.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Tire Friction (lat.)",
|
||||
&vehicle.tireFrictionLateral, 0.05f,
|
||||
0.0f, 5.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Anti-Roll Bar (N/m)",
|
||||
&vehicle.antiRollBarStiffness, 50.0f,
|
||||
0.0f, 20000.0f))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat3("Seat Offset",
|
||||
&vehicle.seatOffset.x, 0.01f))
|
||||
modified = true;
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(?)");
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Legacy single driver seat; only "
|
||||
"used when the Seats list below is "
|
||||
"empty.");
|
||||
|
||||
ImGui::Text("Seats (%d)", (int)vehicle.seats.size());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Add##seat")) {
|
||||
vehicle.seats.push_back(VehicleSeat());
|
||||
modified = true;
|
||||
}
|
||||
|
||||
int removeSeat = -1;
|
||||
for (size_t i = 0; i < vehicle.seats.size(); i++) {
|
||||
VehicleSeat &s = vehicle.seats[i];
|
||||
ImGui::PushID((int)i + 1000);
|
||||
Ogre::String label = s.name.empty() ?
|
||||
"Seat " + std::to_string(i) :
|
||||
s.name;
|
||||
if (s.driver)
|
||||
label += " (driver)";
|
||||
if (ImGui::TreeNode(label.c_str())) {
|
||||
char seatNameBuf[256];
|
||||
snprintf(seatNameBuf, sizeof(seatNameBuf), "%s",
|
||||
s.name.c_str());
|
||||
if (ImGui::InputText("Name", seatNameBuf,
|
||||
sizeof(seatNameBuf))) {
|
||||
s.name = seatNameBuf;
|
||||
modified = true;
|
||||
}
|
||||
if (ImGui::DragFloat3("Offset", &s.offset.x,
|
||||
0.01f))
|
||||
modified = true;
|
||||
if (ImGui::Checkbox("Driver", &s.driver))
|
||||
modified = true;
|
||||
if (ImGui::SmallButton("Remove##seat"))
|
||||
removeSeat = (int)i;
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (removeSeat >= 0) {
|
||||
vehicle.seats.erase(vehicle.seats.begin() + removeSeat);
|
||||
modified = true;
|
||||
}
|
||||
if (ImGui::DragFloat("Seated Char Scale",
|
||||
&vehicle.seatCharacterScale, 0.01f,
|
||||
0.5f, 1.5f))
|
||||
modified = true;
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(?)");
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Uniform scale applied to the "
|
||||
"character while seated (restored on "
|
||||
"exit); < 1 shrinks the sit pose to fit "
|
||||
"the cabin.");
|
||||
if (ImGui::DragFloat("Camera Distance",
|
||||
&vehicle.cameraDistance, 0.05f,
|
||||
1.0f, 50.0f))
|
||||
|
||||
@@ -288,7 +288,7 @@ struct GUIListener : public Ogre::RenderTargetListener {
|
||||
ImGuiWindowFlags_NoInputs);
|
||||
// if (ECS::get().get<GUI>().enabled)
|
||||
// ECS::get().get<App>().app->setWindowGrab(true);
|
||||
ImGui::PushFont(bigFont);
|
||||
ImGui::PushFont(bigFont, 0.0f);
|
||||
ImGui::TextWrapped(
|
||||
"%s",
|
||||
"This game does not autosave. Please use save function to keep your state");
|
||||
@@ -307,7 +307,7 @@ struct GUIListener : public Ogre::RenderTargetListener {
|
||||
ImGuiCond_Always);
|
||||
ImGui::Begin("Narration...", NULL,
|
||||
ImGuiWindowFlags_NoTitleBar);
|
||||
ImGui::PushFont(midFont);
|
||||
ImGui::PushFont(midFont, 0.0f);
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
GUI::NarrationHandler *narration =
|
||||
ECS::get<GUI>().narrationHandlers.front();
|
||||
@@ -356,7 +356,7 @@ struct GUIListener : public Ogre::RenderTargetListener {
|
||||
ImGuiCond_Always);
|
||||
ImGui::Begin("Narration...", NULL,
|
||||
ImGuiWindowFlags_NoTitleBar);
|
||||
ImGui::PushFont(midFont);
|
||||
ImGui::PushFont(midFont, 0.0f);
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
ImGui::TextWrapped(
|
||||
"%s",
|
||||
@@ -454,8 +454,8 @@ struct GUIListener : public Ogre::RenderTargetListener {
|
||||
ImVec2 size = ImGui::GetWindowSize();
|
||||
float window_width = size.x;
|
||||
float window_height = size.y;
|
||||
ImGui::PushFont(bigFont);
|
||||
ImGui::TextWrapped("%s", "Booo!!!!");
|
||||
ImGui::PushFont(bigFont, 0.0f);
|
||||
ImGui::TextWrapped("%s", "Booo!!!!");
|
||||
float extra_pixels = 20.0f;
|
||||
float button_width = 0.0f;
|
||||
float buttons_height = 0.0f;
|
||||
|
||||
@@ -283,8 +283,9 @@ void drawRectPreview(nlohmann::json::json_pointer ptr,
|
||||
// Outline if selected
|
||||
if (rectName == name)
|
||||
draw_list->AddRect(p_min, p_max,
|
||||
IM_COL32(255, 255, 0, 255), 0.0f, 0,
|
||||
2.0f);
|
||||
IM_COL32(255, 255, 0, 255), 0.0f,
|
||||
2.0f,
|
||||
ImDrawFlags_None);
|
||||
}
|
||||
ImGui::Dummy(canvas_sz); // Advance cursor
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user