Jump to content

Atomic Walrus

Members
  • Posts

    12
  • Joined

  • Last visited

Recent Profile Visitors

The recent visitors block is disabled and is not being shown to other users.

Atomic Walrus's Achievements

  1. YMMV, I have some hacky networking tricks enabled here to allow for more simultaneous objects in SP, but the actual physics behavior is stock 4.x with the fixed contact constraints from above.
  2. Over on the Discord there's been some work on the stock physics code going on, and I thought it could be helpful to share some of my notes on Rigid and RigidShape/Vehicle and how to get the most out of the stock systems. This is known information in one form or another, but since the physics have always been glitchy it isn't frequently discussed or compiled. Contact vs Collision & contactTol Collision events are handled in one of two ways depending on the velocity: 1) Very slow collisions are treated as "contacts," and use a solver specifically designed to handle stable resting contact (it involves springs that separate objects that are overlapping). This solver produces more reasonable results*** when objects are slow, but is not intended to handle proper impacts where force is reflected (bounce from restitution). 2) Normal-speed collisions are solved using conservation of momentum, which is better at simulating the outcome of impact events, but not great at simulating resting or sliding contact. The cutoff velocity between these two is controlled by the contactTol datablock value. Contacts with a converging velocity less than contactTol, in m/s, are handled with the contact constraint. Above that speed they are solved as full collisions. The stock value is 0.1 m/s, which means "contacts" only occur when the object is nearly stationary -- or when it's sliding along a surface, since the velocity in question is specifically "speed into the surface being contacted," not overall speed in any direction. *** "But isn't the contact solver notorious for being broken and letting objects sink into surfaces?" True. However this has been (hopefully) fixed by some work going on over in the Discord that should appear in the dev head <eventually?>. For those who want to mess with this patch immediately (YMMV, no money back, etc.): // Go to RigidShape.cpp or Vehicle.cpp, depending on whether you're using 4.x or 3.x respectively, // to the function "resolveContacts," and change the two instances of: mRigid.getZeroImpulse(r,...) // to: (mRigid.getZeroImpulse(r,...) / dt) // This is a unit conversion from Impulse (mass * instant delta velocity) to Force (mass * deltaV over time) // It needs to be done to fit the units of the other terms in the spring model. // The output of the whole spring calculation is converted to impulse (force * dt) later on //And then for reasons I don't want to get into much today -- // this is a reference velocity for damping, it needs to be appropriate to the dimensions of the object and should be autogenerated // -- change this term: (vn / mDataBlock->contactTol) // to: (vn / 2.0f) Collision Buffer Space & collisionTol The detection system uses a buffer space of collisionTol (m) to basically "project" its collision geometry out around it. For a collision to be engaged with it needs to be both within that projected test buffer region and have a converging velocity (or be so slow that it's considered a "contact", see part 1). In practice this means that the collision model of an object behaves like it is slightly extruded beyond the hull geometry. Since collision hulls must be convex, this is not hard to imagine visually (ascii art time): _ _ _ _ _____ | | | ______ | / _____ 1= > | /| / <== 2 | \| \_____ | \ ______ | | Above, neither reacts to the other. Their buffers overlap, but to collide one buffer needs to contact the other's actual collision hull. Below, both objects will detect and respond to the collision. Note that the actual collision hulls are not overlapping with each other. If they were, we would have already "missed" the collision (see practical limits below for more on that) _ _ _ _ _____ | | _|____ / |____ 1=> /| /| <== 2 \| \|____ \__|____ | | ((Too technical: Note that the direction of the "expansion"/"extrusion" of the collision hull is not from the center outwards. (It appeasr that way in the drawings for simplicity). The projection technically occurs in the contact normal direction on a per-interaction basis, but that's fairly confusing to think about.)) You can see this in-game by increasing collisionTol and then looking at the gaps between objects and the surfaces they are resting on. Note that from an art perspective, you can resolve this gap by making the collision hull slightly smaller than the visual model (same thickness as the collisionTol you expect to use). Objects handle high-speed collisions and stacking scenarios better with higher collisionTol, but if the value is too large for the size of the object you'll have trouble making it work visually, and the results may start to get floaty and unrealistic. When in doubt, add a bit more collisionTol and hide the buffer space with the model. Default value is 0.1m, 10 cm. That's plenty for basic prop objects and probably even slower vehicles, but I'd suggest 0.25m or even 0.5m on a vehicle if the model is on the larger side. Integration Rate: Resolution vs. Performance The stock sim tickrate of 32hz is usually fine for the average shooter or other moderately-paced action game, but it's too slow for physics simulations of interacting rigid-body objects when you don't have fancy technology like rollback, group interaction solvers, continuous collision detection, etc. For this reason the physics tick is divided up into sub-frames that are "integrated" into the final deltas for the object's simulation tick. If a normal sim tick is 32ms, and the "integration" value for a RigidShape datablock is 4, the object will run four 32/4 = 8ms physics frames and then integrate the results to simulate the object for the full 32ms of the tick. This increases the temporal resolution of the physics of the object, giving you better results at high speeds with fewer collision misses, in exchange for multiplying the processing time spent on physics calculations. Set this for the expected speeds the object will encounter. I've found that reasonable values are in the range of ~3-16, with the high end being reserved for extra-fast vehicles like jets. 2 can be unpredictable and bouncy and is not recommended. Objects may not go to sleep properly or may fall through surfaces at moderate speeds if integration is set to 1. Practical Limits: Speed, Complex Interactions, Collision Misses It's still not going to be a box-stacking simulator. Even with fixes and tuning this won't replace something like PhysX for a VR game where you want to make physics puzzles that don't behave like they're from Trespasser (1998), or maybe Source multiplayer prop physics at best, where stuff kind of stacks but it's laggy and wobbly compared to singleplayer. You won't even be able to reliably place a pile of crates to drive through like a cheesy action movie, unless you place them all "at rest" and only wake them up as the car drives through them. What this does allow is mostly reliable use of Vehicle objects without constantly worrying about them falling into space or getting flung into the atmosphere if they're parked too close together. The trick to making your vehicles safe to crash is making sure you're never missing a significant portion of your collision geometry's chance to catch a collision. If the geometry is a box you don't have much to work with, so missing any particular collision event is worse than on a complex mesh. The more verts and edges in the model, the more collision events it generates (at the cost of more CPU usage doing collision testing, potentially way more). Returning to the ASCII art in a 1D scenario now, it's possible to figure out the integration rate and/or collisionTol you need for a given velocity to never miss a collision: integration = 4 tickrate = 32ms (roughly) subframe = 8ms velocity = 200m/s collisionTol = 0.25m T=0s | T=1s dX=0m | dX=1.6m T=0s -- 0.25m (collisionTol) ===> > | | -------- 1.25m (dist to wall) T=1s |=|=> > -- 0.35m (missed collision by) To make this scenario impossible, contactTol needs to be > dX. Given that integration is the performance-limiting setting, fix integration @ 8 subframe = 4ms = 0.004s velocity = 200m/s Find collisionTol: collisionTol > 200m/s * 0.004s collisionTol > 0.8m Or alternatively, for a fixed collisionTol of 0.6m, find the optimal integration for 200m/s max vel: 0.6m > 200m/s * 0.032 / integration integration ticks > 200m/s * 0.032 s/tick / 0.6m integration > 10.67 (integration should be at least 11). A long-winded way of saying collisionTol should be >= maxVel * TickSec / integration so that you never miss a collision event. For the standard test objects and default settings, integration = 4 and collisionTol = 0.1, so the max speed where 100% of collisions are detected is: 0.1 m >= V m/s * 0.008s -> V <= 0.1m/0.008s -> 12.5m/s, or around 25mph. To be clear this is generally overkill; Most vehicles will not have perfect box collision hulls, nor would most interactions involve a single vert vs a plane like above even with cube hulls and flat surfaces. This comes up in worse case scenarios, like dropping a cube onto a perfectly flat surface when everything is axis-aligned. You either catch those corner verts hitting the floor, or you miss the entire collision because the vertical edges are perfectly perpendicular to the surface in this contrived case and will fail to interact with the plane. This is contrived, but not that uncommon due to how editors spawn objects, so it's still worth knowing the exactly limits. When a cube lands on a plane hitting corner-first, which is 99.9% of the time once objects are moving from their editor-placed staring poses, you can actually miss that initial corner vert vs plane interaction and still catch the overall bulk interaction since the cube's edges will also trigger collision events. In practice you can often undershoot this optimal tuning for performance reasons and still be fine, so feel free to undershoot on both of these and increase them as needed.
  3. Just a heads up that the one solution posted there, setting MountedMask on every SceneObject::setTransform event is going to add network overhead for every mounted object (similar to how my dumb hack adds network overhead for turrets). Doing this basically makes the MountedMask have no function, since the mounting info will be sent every update packet. Mounted ShapeBase objects call setTransform every tick in processTick. The reason for these setTransform calls is that we want mounted objects to be culled correctly, set their detail levels from distance, have radius damage applied, be raycasted against, etc. and so they need to have the correct transform stored. Just modifying the getTransform function to return the mount transform won't work, because not all interactions with SceneObject transforms go through getTransform (you'll find lots of code directly accessing mObjToWorld, mWorldBox, mWorldToObj, etc.). stream->writeInt( gIndex, NetConnection::GhostIdBitSize ); if ( stream->writeFlag( mMount.node != -1 ) ) stream->writeInt( mMount.node, NumMountPointBits ); mathWrite( *stream, mMount.xfm ); The above writes are going to be a minimum of 64 bytes for every mounted object, in every packet. It's not a trade-off I would make in my project because of how many things I mount to Vehicles, but obviously YMMV (my hacky solution with turrets is just as bad, but it was meant to be a temporary bandaid and I forgot to come back to it). I'm going to throw out a different quick-fix option that sort of has less overhead: Just de-ghost everything that isn't in scope, even if it's not waiting to send an update. I say "sort of" less overhead because this functionality will cause "static" ghostable objects to de-ghost and need to be re-sent to clients based on distance, something the current system avoids by not killing ghosts unless they are actively sending updates. It will, however, be much closer to the "expected behavior" people have of this system; I think most users would assume that out of scope objects get de-ghosted immediately, instead of next time they set a mask bit. You also still have the option to just make your static objects ScopeAlways (like TSStatic), which could make more sense anyway. So here is the change to de-ghost things immediately whether or not they have a mask bit set. I'm just swapping mGhostFreeIndex (first free ghost ID) in place of mGhostZeroUpdateIndex (first ghost with no updates). Edit: Fixed a small bug, should only increment skip count on objects that are queueing to send an update, otherwise the other objects will accumulate unnaturally high update priority netGhost.cpp, NetConnection::ghostWritePacket ... for(i = 0; i < mGhostFreeIndex; i++) // CHANGE HERE: mGhostFreeIndex replacing mGhostZeroUpdateIndex, consider all ghosts for de-ghosting even if not waiting to update { // increment the updateSkip for everyone... it's all good walk = mGhostArray[i]; if (i < mGhostZeroUpdateIndex) // CHANGE HERE: Only increment the skip count on things actually waiting to update walk->updateSkipCount++; if(!(walk->flags & (GhostInfo::ScopeAlways | GhostInfo::ScopeLocalAlways))) walk->flags &= ~GhostInfo::InScope; } if( mScopeObject ) mScopeObject->onCameraScopeQuery( this, &camInfo ); doneScopingScene(); for(i = mGhostFreeIndex - 1; i >= 0; i--) // CHANGE HERE: mGhostFreeIndex replacing mGhostZeroUpdateIndex, consider all ghosts for de-ghosting even if not waiting to update { // [rene, 07-Mar-11] Killing ghosts depending on the camera scope queries // seems like a bad thing to me and something that definitely has the potential // of causing scoping to eat into bandwidth rather than preserve it. As soon // as an object comes back into scope, it will have to completely retransmit its // full server-side state from scratch. if(!(mGhostArray[i]->flags & GhostInfo::InScope)) detachObject(mGhostArray[i]); } ... What this does is have it iterate through the full active ghost list, instead of just the "has updates" portion (zero to mGhostZeroUpdateIndex, they're sorted). The detachObject function is prepared for this possibility; When called on an object without an update queued it will set a mask bit for that object and then re-sort the list so that it is in the "updates pending" portion of it. I tested this and it resolves the issue in question (with the other fixed discussed previously disabled). An alternate solution that would have even less overhead would be to have mounted objects link their scoping state and ghosting behavior entirely to the thing they're mounted to, but that's going to be a bit more complex from a code perspective so I'll have to circle back on that. ------ Some extra discussion... While we're looking at it, that comment from either 2007 or 2011 is worth additional consideration. In my own project I'm planning to disable render distance based de-ghosting in favor of putting client ghosts of game objects into a "sleep" state where they don't process ticks, advance time, render, or receive network updates. It accomplishes the same thing from a performance-saving perspective, without the need to delete and recreate game objects on the client and receive new initialUpdate packets. I'll share this code and any other improvements I make to the scoping behavior when it's ready, but no promises on the timeline (I'm notorious for planning to do things like this and then getting too busy with my day job). I think you'd optimally want both of these system to exist together, so that objects can still go in and out of scope when desired, OR be put to sleep, depending on what fits the project.
  4. *Edit: Accidental early post, should all be here now. I just ran into an issue related to this same thing, but with a different object-pairing interaction. I seem to have fixed this specific case with turrets years ago and just forgot about it until it came up in another situation. The turret object isn't sending any network updates while it's mounted and un-manned, so it's never actually getting de-ghosted when it goes "out of scope" and this messes up the assumptions of the mounting system networking. First, how it's supposed to be working: When an object is more than visibleDistance from a given client's camera, it will no longer be flagged as "in scope" for that client on the server. Next time the server sends an update about this object to that client, it will inform them that this object is out of scope and the client will delete its ghost of the object. When the object returns to within visibleDistance of the client, the server will tell the client to create a new ghost for the object, and then will force an "initial update" packet for the object, which means it sets every mask bit on. As part of "initial update" the object will send the ghost ID of the object it's mounted to. If the client doesn't have a ghost of that object yet, the object will keep re-attempting to send the mount ID until it succeeds (it will keep re-setting MountedMask if the mount's ghost doesn't exist yet). That last point is supposed to prevent the scenario you're experiencing from occurring, however when mounting support was added to the TurretShape they missed something from the Player implementation which I'll get to in a second. Here's what's going wrong: The vehicle is out of range to the client, goes "out of scope" to the client. So does the turret. Nothing's deleted yet, this is just the server deciding that these objects are out of range to this client's camera. As in (2) above, the server will only tell a client to kill its ghost if that object is generating network events in the first place. T3D vehicles send network updates all the time, even if they're asleep from a physics perspective. This means they de-ghost as soon as they go out of range. Stock T3D turrets don't send any updates when they're unmanned and stationary. And when they're mounted, they're considered completely stationary. The vehicle de-ghosts. The turret doesn't de-ghost because it's not sending any updates while out of range. You come back into range, the vehicle re-ghosts and sends its initial update packet. The turret didn't need to re-ghost, it's just still there, but your client sees it as mounted to an object that no longer exists (the old ghost of the vehicle, that was deleted) and so behaves as though unmounted. The turret sees no reason to update the client about its mounting status, because MountedMask was never set (it would have been set during initial update if the turret had been re-ghosted). When you do anything to the turret in the editor, it calls SceneObject::inspectPostApply, which sets MountedMask and fixes the glitch. So what's different about Player objects that prevents this? It turns out that when Player objects are mounted they force basic positional network updates: bool Player::updatePos(const F32 travelTime) { PROFILE_SCOPE(Player_UpdatePos); getTransform().getColumn(3,&mDelta.posVec); // When mounted to another object, only Z rotation used. if (isMounted()) { mVelocity = mMount.object->getVelocity(); setPosition(Point3F(0.0f, 0.0f, 0.0f), mRot); setMaskBits(MoveMask); return true; } ...[etc]... The velocity and position components are secondary, the main point is that this sets MoveMask and thus forces an update packet, meaning that mounted Player objects that go out of scope range will be de-ghosted. When I looked at my code, it turns out I'd fixed this in my turrets a long time ago, and the comment there even indicates that I'd specifically done it to resolve this scoping issue: void TurretShape::processTick(const Move* move) { [...code...] if (!isGhost()) updateAnimation(TickSec); // addthis if (isMounted()) { setVelocity(mMount.object->getVelocity()); // this sets PositionMask at the Item level, forcing a network update and fixing scoping/de-ghosting } // endhere [...more code...] setVelocity sets the PositionMask (from Item). You could just set PositionMask manually instead, but I prefer that mounted objects do have the correct velocity values so this handled two things at once. Technically even setting PositionMask is really overkill, it triggers a totally un-needed write of the transform. You could actually just use any mask bit, including an unused one that won't result in anything being written during packUpdate. It just needs to trigger a ghost update.
  5. Thanks for doing all this and releasing it! This is 100% functional in 2021 with the Index + controllers, still using SDK build 1.0.17. I tried throwing 1.16.8 in, but enough has changed that I'll probably need to upgrade a few builds at a time, and for the moment this is working perfectly. Only one proper technical issue so far: The LOD system doesn't function in VR (take a look a the vehicle in the empty terrain map). Will investigate. At the moment I just have it hacked to always use max LOD. --- TL;DR portion below, discussing gameplay implementation details for VR in T3D Right now you can reach parity with No Man's Sky by simply using the seated mode. This is more than sufficient for vehicle cockpits, and it's workable for a player character if you don't mind that it's not proper roomscale (you can walk away from your Player body). Like in NMS, you can just tell the player they've moved too far from their character body and do an auto-recenter. If concerned about the player putting their head through geometry, you can run a raycast every tick between the character's head and the VR HMD. That plus putting the ShapeBaseImage on a tracked controller is sufficient for your basic singleplayer experience as long as you don't have too many narrow doorways. The player never sees their own body, it's just used for collision volume and taking damage, so they won't care that your hands and gun don't line up with the rest of the body. I think for a basic crossplay multiplayer experience, getting the Player to line up its aim angle with the held VR gun should be sufficient. Limited, because it eliminates all the fun VR social interface of gestures and head tracking, but functional for putting VR and flat players into the same world. Your avatar will do odd stuff if you throw the gun around, but nothing more odd than what you can do now by spinning with your mouse. I'm also running your GuiOnObject2 implementation for VR GUIs. Working on a hybrid of laser pointer and touchscreen, where you can point and click the trigger from a distance, but at very close range it will register as an auto-click like a touchscreen. It needs basic collision detection (prevent the hand from going through the screen) to work the way I really want it, where you would be able to touch and swipe to move a slider. Will share whatever I come up with on this if it's any good.
  6. This reply is only life half a year old, which is like 3 days under modern time rules. Or is that 3 decades? Well either way, if it helps someone... I'm going to presume you're seeing correction packets (writepacketdata/readpacketdata) which are what's actually causing the stutter. The default turret implementation doesn't properly account for the client-predictive/server-authoritative model control objects work under. In my experience, it's the deeply integrated client-server system of Torque that most people bounce off of, but that's also one of its biggest strengths if you're actually making a multiplayer game. I was certain I'd posted about this at some point, and indeed if you journey back to the old GarageGames forums here is my fix: http://www.garagegames.com/community/forums/viewthread/130721 What this does is create a "render" version of the rotation variable and uses that to determine render orientation. This version can be safely modified by client-only code for interpolating between ticks, without altering the actual simulation variable for turret rotation (interpolation doesn't occur on the server, so interpolating the simulation rotation would put client and server out of sync). Follows the same model as every scene object having a separate simulation and render transform to store pos/rot.
  7. What happens when you fire from 3rd person? My hypothesis is that the server doesn't actually use the 1st person model, and so bases its muzzle vector on the weapon's position in your 3rd person model animation. If that's true then the vectors should line up when you fire in 3rd person. If they still don't then I'm wrong and it's.. something else, and I have no clue.
  8. I can't remember which version added TSStatics working as mountable props, but it definitely functions in stock 3.10. Since they don't have any game functionality they're a good low-overhead choice for mounting a visual prop to a vehicle or whatnot. For most objects, the following code pasted into the end of processTick and advanceTime is sufficient to get them to behave correctly when mounted: if (isMounted()) { MatrixF mat; mMount.object->getMountTransform( mMount.node, mMount.xfm, &mat ); Parent::setTransform(mat); } ShapeBase also had this functionality added in a recent version -- try mounting a Cheetah to another Cheetah in stock 3.10!
  9. Mounting was moved to the sceneObject quite a while back, so basically everything in the scene can be mounted to anything else. Mount points in the models (mount0, mount1, etc) are still used, but not required; for example, if you mount an object to a mount slot that doesn't have a defined mount point will simply mount at the origin (center). You can use the offset coordinates in the mountObject command to put the mounted shape anywhere you want. Keep in mind that while all SceneObjects can mount, not every object has a defined behavior when it mounts; Mounting is just a theoretical association, and the mounted object must use that association to place itself at the mount point. Take a look at how TSStatics handle mounting, as they are the simplest functional mountable object. Open up TSStatic.cpp and search for "ismounted" (no quotes), you will find very basic code for making a mounted object actually visibly stick to its mount point. Depending on your object's inheritance, you may need to copy these bits of code into the relevant functions. If your object has a physics simulation, either stock or external, you'll probably want to pause it while the object is mounted.
  10. Because of this line: mShapeInstance->castRayEA(start, end, info,0,mDataBlock->HBIndex) in player::castRay, the hitboxes must always be in the highest (first) detail level. Detail zero, as passed in the function above before the hitbox index. If you wanted to do something like define a specific detail size as the hitbox detail you would have to loop through the model's details, mShape->details (for (U32 i = 0; i < mShape->details.size(); i++)) testing mShape->details.size until you found the one you wanted to use, then send that detail level's index instead of zero. If you didn't want to run that loop on every raycast, you could search for and store the hitbox detail level index during preload. Alternatively, I used to just make the boxes invisible with a material.
  11. Stick this at the end of mathTypes.cpp (for some reason all the mathutil console function hooks are here so you won't need to add any headers): DefineConsoleFunction( VectorGetMatrixFromUpVector, TransformF, ( VectorF vec ),, "@Create a matrix from the up vector.\n\n" "@param VectorF (x,y,z) up vector.\n" "@return TransformF.\n" "@ingroup Vectors" ) { MatrixF outMat; MathUtils::getMatrixFromUpVector(vec, &outMat); return outMat; } The data that comes back will be in the "TransformF" format which means words 0-2 are position and 3-6 are rotation (axis angle). In script you can do something like this: %mat = VectorGetMatrixFromUpVector(%normal) %outTrans = %object.getPosition() SPC getWords(%mat, 3, 6); %object.setTransform(%outTrans); All of your placed objects will have 0 rotation around the normal (because the normal vector obviously contains no info about that rotation) like the mines. If you want to rotate about the normal it should be a simple matrix multiplication like this: void WorldEditorSelection::rotate(const EulerF &rot) { for( iterator iter = begin(); iter != end(); ++ iter ) { SceneObject* object = dynamic_cast< SceneObject* >( *iter ); if( !object ) continue; MatrixF mat = object->getTransform(); MatrixF transform(rot); mat.mul(transform); object->setTransform(mat); } } But again you need to expose some math helpers to script (or just attach a function like this to sceneobject and expose THAT to script). The script-end handling of object rotation isn't very good in general. I'd like to have euler angles exposed (along site the current axis angle interface) and some rotation functions that can do both of the relative rotations the editor handles (world and local yaw/pitch/roll).
  12. Just a heads up on the existing energy system: It's part of the client-predictive networking and was intended to be used mainly for energy related to movement (or other client-predictable events). Since it's part of writePacketData it will: -Only be updated to controlling clients (during a correction) and -Trigger correction packets if modified by a server-initiated event (as opposed to a client-predictable event initiated by a "move"). +Allow movement to be energy-dependent without worrying about being out of sync with the server. For example it would be network-safe to scale player movement speed by current energy level. It's not the best choice if you're doing script-based abilities that drain energy, want a pool for weapons (not client predicted in stock), shield energy that drains when you take damage, etc. It will work but the correction packets will cause movement stuttering and waste bandwidth (sending the whole correction packet when all the client needed was 5 bits).
×
×
  • Create New...