• Advertisement
Sign in to follow this  

Unity Optimizing memory layout (and reducing load times)

Recommended Posts

Disclaimer: I am writing my own thing, so this does not pertain to UE, Unity or any other professional engine.

I'm taking a critical look at my actor code and trying to figure out how best to serialize/deserialize the data for disk and network streaming. The problem right now is that there are inherently a lot of allocations happening for any given actor. 

Initializing the "RPG" component of a pawn actor alone necessitates a slew of allocations as there are quite a few subcomponents that have a metric crapton of fields/arrays that do not have fixed sizes. I'd like to not only reduce load times (which in all fairness aren't an issue at this point), but also memory fragmentation as I haven't really implemented any manner of a compactification in my memory manager.

What are the common approaches and things to streamline in this case, and - more importantly - are they worth it?

The most obvious ones I can think of are:

1) convert dynamic strings to fixed-length char buffers
2) convert any vector-style arrays to fixed-size arrays
3) use field packing and relative offset pointers to coalesce several small chunks of data into a fixed-sized pool (kind of the way uniform buffers work)
4) resolve internal pointers of referenced objects to indexes into an array or hashes into a hashmap (this probably requires some kind of a an additional solution to cache the actual pointers)

The issue here is that I'm working with abstract objects that can be extended fairly arbitrarily. That being said, while I feel reserved about capping objects containing arrays stored within an array stored within an array, I don't really see the harm in setting some fixed size/length caps on certain variables at the expense of storage. My memory allocator is generously aligned anyway, so there's already some inherent waste.

The reason I'm asking is because I'm at a point where I don't have a real-world use case in my hands and I can't really tell if worrying (or worrying to what extend) about this is worth it. Rewriting this stuff is a lot of work, but I'd rather do it before I compound the issue further. And also to have the infrastructure in place as I'd like to write some form of serialization solution in the near future.

Share this post


Link to post
Share on other sites
Advertisement

I am aware of the basic rule of premature optimization :).

Hence, like I mentioned on more than one occasion, my concern isn't so much about speed as it is about practicality and memory fragmentation. I'm looking for ideas, possible input on how a commercial engine might do this and trying to figure out a middle ground, which would allow me to serialize my data without too much hassle while not bending over backwards to then deserialize it.

In this case versioning my code so that I can compare microseconds is the last thing on my mind (okay, maybe not the last), because of the amount of work involved. I don't just want to dig into it and start profiling - instead, I want to write good architecture that doesn't need to be redesigned in a couple of months.

Share this post


Link to post
Share on other sites

In that case, your four items are the best options I know of for the general case.  

#1 and #2 avoid allocations and reduce memory time. #3 and #4 (often done with an offset table) can help minimize disk time. Do all four and you reduce both disk and memory time. The more packing you do up front the more fragile the system is to modification and extension, so it is a balance.

You mention network, so the extremely high overhead of data transfer is well worth additional processing. Memory fragmentation will be specific to the game; level-based games where EVERYTHING can be loaded at once and EVERYTHING can be discarded at once fit the model of a large bundle, but they aren't particularly extensible. Longer-running systems need more handling of object lifetimes and object pools. While there is a ton of literature on various scenarios, they still boil down to special cases the options you listed: minimize allocations and transforms, minimize expensive I/O. 

Share this post


Link to post
Share on other sites

'd argue it's a bit strange to worry about memory fragmentation, while possibly arguing for fixed-length arrays (where most of the space is unused) and talking about how your memory allocator has 'generous' alignment. Aren't you just moving the fragmentation around?
 

I'm aligning allocations to 16 bytes by default. Overall that's a whole lot of wasted space. That's what I meant by generous.

What I'm arguing for here is "clumping" allocations more compactly by coalescing as many of them as possible. The only place where this matters presently in practice is when loading a new level. I'm sorta structuring stuff with world streaming in mind, though, so that's kind of the bigger general concern on my mind.

Personally, I don't recommend trying to make everything into flat data structures for fread/fwrite - I've spent literally weeks debugging problems that arose from code that did this, but which failed to anticipate some future change (to the data structure, to alignment, or padding, or the shift from 32 to 64 bits), and silently trashed or lost some data as a result. But, I know some people will disagree and say that it's worth the effort. Still, the big problem for me is that when you change your format, all your data is dead. A careful versioning system can help.

Good points all around. I was thinking of sidestepping architecture/compiler-specific stuff (32/64-bit, struct member ordering and endianess specifics) by writing a strongly typed serializer that breaks everything down to basic types and fills in each field individually. So while the data is loaded from disk in one big chunk, the deserializer still walks through each field individually and does any necessary conversions. This SHOULD avoid data loss on format change. 

Basically do something like this:

struct mytype_t { 
   rect_t field1;
   int32 field2;
   int16 field3;
};
IDataSerializer ds;
mytype_t mt;
// do runtime reflection that determines field order and sets up offsets/sizes/types
DEF_SERIALIZED_TYPE(ds, mytype_t, field1, field2, field);
DEF_SERIALIZED_TYPE(ds, rect_t, lft, top, rgt, btm);

// do the same for basic types - int32, int16, etc

// use the definition above to write the object tree to disk, breaking everything down to basic types
ds.serialize(stream, mt);
// if order of fields is not changed, fill in the entire structure one basic type member at a time
ds.deserialize(stream, mt);

I couldn't really describe how I would implement versioning in a robust way right off the bat.

One thing that is worth doing, if practical, is replacing dynamic length strings with string IDs, hashes, or indices. Unreal has this via the FName concept. Your structures that contain strings end up containing small fixed-sized IDs/hashes/indices, while the actual text lives in a hash table somewhere, and can be stored quite efficiently as one long contiguous string at the end of a file. Sometimes the actual text doesn't need to be loaded at all (e.g. if you use hashes, and just check for equality). Different approaches have different pros and cons.

Indeed, many of my strings have a hash and I'v gone to some lengths to avoid data duplication by only storing a HASH-type variable where needed.

You mention network, so the extremely high overhead of data transfer is well worth additional processing.
 

This is a very good point. I did mention networking somewhat prematurely, though, as I don't really have any real multiplayer at this time. Worth keeping in mind, though.

Share this post


Link to post
Share on other sites

You say, "the deserializer still walks through each field individually and does any necessary conversions"

Frob says, "data conversions and processing slow down serialization".

Sounds like you're trading away one problem only to get another. Without having measured anything yet, I don't think you're in a position to make that decision either way.

There is also no decent way to do versioning for arbitrarily-nested objects. You can do it at the level of individual struct types where those structs contain basic primitives, but that's about it.

Share this post


Link to post
Share on other sites

concern isn't so much about speed as it is about practicality and memory fragmentation

For 64-bit builds, fragmentation is nowhere near as scary as it used to be. Modern address space is big!
Having a shitload of tiny objects isn't necessarily bad for fragmentation either. Having a shitload of tiny that are allocated at the same time but have wildly different lifetimes is bad for fragmentation - it's those tiny stray objects with long lifetimes that clog up your address space. If you create 10k objects at the start of a level, and then destroy all 10k objects at the end of the level, then there's no problem.
Many level/scene based games that I've worked on have enforced this constraint upon us -- any allocation created during a level must be free'd at the end of that level, or the game will forcibly crash to loudly let you know that you've broken the contract. 

The issue here is that I'm working with abstract objects that can be extended fairly arbitrarily ... objects containing arrays stored within an array stored within an array

You can go back a few steps in the design and make less of an object soup in the first place ;)
As much as I love to shit on the ECS-anti-pattern, the idea of sorting and segregating all objects by type so that they can be treated as massive buckets of bits that get streamed about the place is a very good feature.

1) convert dynamic strings to fixed-length char buffers 2) convert any vector-style arrays to fixed-size arrays

That sounds extreme as in Kylotan pointed out - this is basically deliberately creating fragmentation in order to avoid it :wink:
Instead of this, I'd strive to get to a situation where as many objects of the same type with similar lifetime are allocated at the same time/place. e.g. in a serialization format, this could mean that when someone asks to write a string to the file, you instead add it to a buffered collection of strings and write a placeholder word which will be patched up with a string id/offset at the end. Then at the end of the serialization process, go ahead and write all of the strings into the file end-to-end, and then go back and patch up those id's/offsets. Then when deserializing, don't create one allocation per string, load that entire chunk-of-strings as a single object and then give out non-owning const char*'s to the objects that need them.
This is really easy to achieve if the objects that you're loading from the file all have the exact same lifetime, such as when loading a level containing a bunch of objects that will exist for the duration of the level. In this case, you can create a single, massive allocation big enough to contain all of those objects and then placement-create all the objects into it. Even better if you can design a serialization system that lets you deserialize in place. i.e. if the constructed version of an object will be no bigger than the serialized version of it, then you can placement-create it over the top of the serialized version. This way you stream a file into a buffer, perform the deserialization process, and now that buffer is full of valid C++ objects without any further allocations being made.
FWIW, I go through the trouble of designing things this way for core engine systems -- e.g. models, materials, textures, animations, background scenes... but don't usually put in the effort for higher level gameplay systems.

I couldn't really describe how I would implement versioning in a robust way right off the bat.

In my use-case, I simply don't. I compile my core game assets into this form, and if the data structures change, I recompile them.
I don't use it for anything that can't simply be recompiled from source assets, such as a user's save game -- that kind of data uses a more bloated serialization system that can tolerate schema changes.

I did mention networking somewhat prematurely, though, as I don't really have any real multiplayer at this time.

While you could re-use disk based serialization for multiplayer networking, it's optimal to not do that. Network code really cares about the number of bits used in the encoding. Disk is slow, but still going to be 500-2000x faster than a crappy DSL line. Network data also doesn't have to persist - if there's a version mismatch then one player is unpatched or cheating and shouldn't be playing! Plus as you're repeatedly sending similar data, you can rely on things like delta encoding against previous gameplay states.

Share this post


Link to post
Share on other sites

This is a follow-up post after a few days of mulling things over. You guys brought up several crucial things, which I tried to take into consideration. Thanks!

Anyway, I took some time to go over my engine code and assess where and how much it would be feasible to start streamlining stuff. Presently I settled my approach to memory allocations with what Hodgman said - not worrying so much about allocations themselves so much as making sure nearby allocations retain similar lifetimes. One of the primary things I was worried about was an I/O mechanism, so I started by automating the serialization code. I haven't been able to do any profiling, because a) I don't have an actual real world dataset to work with yet and b) for now I'm neck deep in debug mode anyway. That being said, I'm using std::is_standard_layout<> to automatically coalesce trivial fields and bin whole arrays of POD-like objects. A lot more preparatory work can likely be done during compile time and I'm hoping to offload some additional infrastructure to the compiler when I finally upgrade to a one that supports constexpr.

As for runtime, using a few of my actual data structures as a baseline, simple field reordering to prioritize grouping of simple/POD-like members and taking of a few steps back to critically assess how I store the stuff in the first place yields results where I can coalesce about 60-80% of data that is read from disk into a single mem copy operation. This does assume some consistency across platforms, though: my current solution to versioning would be to store hashes of object types (which I'm thinking of calculating as a CRC of one long string (which has been stripped of whitespaces) obtained by concatenating all field types of serialized object) in the data file's header. At this point I can still decide to read data as-is, even if the hashes don't match to partially load an object, or - as suggested above - rebuild the dataset.

Share this post


Link to post
Share on other sites
This does assume some consistency across platforms, though

Yeah, that's exactly what wasted our team about a month of coder time trying to fix up. Not only do you have to consider type sizes, but you have to consider alignment, and padding. And changing compiler defaults. And new platforms.

Take a simple struct of `int a; char b; vector3 c;`. This will give you a single unambiguous CRC hash. But, given any arbitrary platform or compiler setting, do you really know what sizeof(a), offsetof(b), or offsetof( c ) are?

You can smash some of this with #pragma pack. Just hope you don't have any objects that have picky alignment requirements.

Edited by Kylotan

Share this post


Link to post
Share on other sites

 

This does assume some consistency across platforms, though

Yeah, that's exactly what wasted our team about a month of coder time trying to fix up. Not only do you have to consider type sizes, but you have to consider alignment, and padding. And changing compiler defaults. And new platforms.

Take a simple struct of `int a; char b; vector3 c;`. This will give you a single unambiguous CRC hash. But, given any arbitrary platform or compiler setting, do you really know what sizeof(a), offsetof(b), or offsetof( c ) are?

You can smash some of this with #pragma pack. Just hope you don't have any objects that have picky alignment requirements.

 

 

Actually, this is where I'm feeling moderately smart right now as my solution is to offsetof()-map each member of a serialized struct during initialization, so even if the alignment changes, I can still revert to reading data one field at a time. Eg, the following is a complete set of required information necessary to map any ordering to any other ordering and it only assumes the programmer not screw up calling one single macro:

// the order of field (f1, f2 and f3) determines the order in which they are written to disk
DEF_SERIALIZED_TYPE(mystruct_t, f1, f2, f3);

The macro uses offsetof() on each member to determine the position of the field for this architecture and compiler.

It also generates a hash for the entire structure, which encodes field types and optionally order. This hash is then written to disk in the header of the data file. It would make sense to split this hash into separate type and field order monikers, though.

When data is loaded, the hash is used to determine a version mismatch. If all fields are accounted for, but do not match the order in which they are stored on disk, local packing data extracted from offsetof() is used to determine ranges that are contiguous and otherwise, as the order on the disk is known, map each field to a new offset.

This pretty much automatically takes care of field ordering issues.

As for type mismatches: my solution is to avoid using stuff like size_t in structures that require serialization and instead convert architecture-dependent types to something more robust, eg int32.

For extended types that have private members or require extra care, I'm using specialized callbacks that can access and properly encode/decode the data.

Edited by irreversible

Share this post


Link to post
Share on other sites

If you're not encoding the offset data into your hash, how will you know that the data on-disk doesn't match the in-memory layout? I'm not talking about changing the ordering of anything, just literally saying that the same objects can be at different offsets even with the order remaining invariant.

Share this post


Link to post
Share on other sites

If you're not encoding the offset data into your hash, how will you know that the data on-disk doesn't match the in-memory layout? I'm not talking about changing the ordering of anything, just literally saying that the same objects can be at different offsets even with the order remaining invariant.

 

It would make sense to split this hash into separate type and field order monikers, though.
 

My mistake - the above statement was meant to infer that both the order and offsets are hashed.

If there's a hash mismatch, though, then the only safe thing to do is to read all the data one field at a time. 

That being said, I'm not in a position to surmise how much this is really an issue IF:

a) the manual ordering of fields in a given struct is sensible
b) all data is either stored in packed arrays or is aligned to start with

Share this post


Link to post
Share on other sites

Yeah, that's exactly what wasted our team about a month of coder time trying to fix up. Not only do you have to consider type sizes, but you have to consider alignment, and padding. And changing compiler defaults. And new platforms.
Given any arbitrary platform or compiler setting, do you really know what sizeof(a), offsetof(b), or offsetof( c ) are?

 I advocate always accompanying the definition of these structures with static assertions that document the assumptions that you're making about how the compiler aligns and pads the structure. If your assumptions are wrong, the code stops compiling. Those assertions then also become the file format specification ("the code is the documentation") for the tool that produces your data files. We were supporting something like 6 platforms at the time pretty easily. Most of them actually had the same packing rules :)

An alternative would be to define your data structures in a different language altogether (some kind of JSON description, etc) and then use that information to automatically generate serialization/deserialization and/or struct definitions (+static assertions) for particular compilers.

The trickiest bit fe hit was that we wanted the 32-bit and 64-bit Windows builds to load the same data files, so we used some template type selection stuff like pad64<Foo*>::type instead of Foo* to force a consistent pointer size on both builds inside these structures, and used the same static-assertions across both builds to ensure they matched... but using pointers in these kinds of structures was pretty rare anyway; we usually used offsets that acted like pointers:

template<class T, class Y=s32> struct Offset
{
	const T* NullablePtr() const { return offset ? Ptr() : 0; }
	      T* NullablePtr()       { return offset ? Ptr() : 0; }
	const T* Ptr() const { return (T*)(((u8*)&offset) + offset); }
	      T* Ptr()       { return (T*)(((u8*)&offset) + offset); }
	const T* operator->() const { return Ptr(); }
	      T* operator->()       { return Ptr(); }
	const T& operator *() const { return *Ptr(); }
	      T& operator *()       { return *Ptr(); }
	static uint DataSize() { return sizeof(T); }
	bool operator!() const { return !offset; }
	Offset& operator=( void* ptr ) { offset = ptr ? (Y)((u8*)ptr - (u8*)&offset) : 0; return *this; }
private:
	Y offset;
};
Edited by Hodgman

Share this post


Link to post
Share on other sites

I'm in a position to tell you that it's a big deal if you expect to run portably. This was an actual problem I've dealt with in commercial game code.

The problem with using a hash is that it's a hash. Hashes are good at telling you that 2 things are different, but it can't tell you how they differ. How do you read in a field at a time, if the only information you have about the offset of each field within that data is a single hash, which doesn't match the hash for your current in-memory layout? If I load in that struct above - int a; char b; vector3 c; - which byte in the file is 'b' - is it byte 5, byte 8, byte 9, or byte 16? Even changing from int to uint32_t doesn't necessarily save you here.

The 'solution' I've seen to this problem, is that if the hash doesn't match, you need to rebuild the data for your platform. Each platform has its own set of data. It's fast to load. It takes up a lot of space and time. Developers get angry when they pull an innocuous code change and find out they need to run the data tools pipeline before they can test their local changes. You win some, you lose some. :)

Share this post


Link to post
Share on other sites

I advocate always accompanying the definition of these structures with static assertions that document the assumptions that you're making about how the compiler aligns and pads the structure. If your assumptions are wrong, the code stops compiling.

 
The experience I have is that there were no assumptions about the padding and alignment, because the person who wrote the data serialiser was never the person writing or editing the structures being serialised. The context is usually something like "this object gets serialised, so only use serialisable types, and the serialiser will do the rest".
 

The trickiest bit fe hit was that we wanted the 32-bit and 64-bit Windows builds to load the same data files, so we used some template type selection stuff like pad64<Foo*>::type instead of Foo* to force a consistent pointer size on both builds inside these structures


Yeah, we had something similar... it broke most of the time when a pointer was hidden inside a struct and nobody noticed... or when someone had typedefed a pointer as something else so nobody spotted it was a pointer.

Occasionally people tried to do 'cute' things such as adding explicit padding - e.g. a spare u32 after a used u32, to make the next object align the same on 64bit platforms. At least until someone else adds another u32 earlier in the file later. This is one of the examples that can be avoided with static_asserts, but that's a mess, IMHO.

Share this post


Link to post
Share on other sites

hm, this was quite interesting to read. so what is the actual bonus compared to using something like https://github.com/USCiLab/cereal to handle serialization? it is fast to get going, yet leaves room to optimize special classes/structs/stuffs for size or speed. it does loose JSON/XML and packed binary. to my rather uninformed self it seemed so fast and efficient that rolling my own wasn't worth the effort. but maybe i am missing out and should? irreversible, what was your reason to write your own and not go for some library?

Share this post


Link to post
Share on other sites

We are doing streaming in our games as well. The problem ist not just the data itself but how they come over the desired media. Loading huge chunks of data from disk is a pain in the ass when your data isnt structured well. As I described in another post, what I do in my game engine is a combination of a proper aligned packed format that consists of 64kb chunks inside the package so reading a single chunk is exactly a full cache on the HDD and memory mapped files with threaded IO to speed things up, loading a single chunk or a range of chunks.

Share this post


Link to post
Share on other sites

irreversible, what was your reason to write your own and not go for some library?

 

Actually it wasn't my initial impulse to write a serializer, but rather to figure out how the heck I was going to structure my code so I could minimize its memory imprint and store my data in a save file while keeping load times under control once the game world starts getting larger. Serialization happened to be a very tightly knit extension of that problem.

The other reason has to do with what most people here would probably call masochism - I try to write as much as possible of my own stuff. I don't recommend it if you're in a hurry to get things done, but writing these kinds of subsystems has kept me quite busy in terms of learning how to structure code in my head and trying to figure out modular, easily extensible solutions. A generic serializer is ultimately an exercise in writing robust templated code and also partially extends to template metaprogramming. The fun part is figuring out how to get away with automating as much of that code as possible. So, in short, it's just good exercise, even if the end result isn't some industry-strength hyper-generic super-optimized multi-platform cross-compiler beast. Besides, without worrying to boot about alignment it's not that big of a task - it took me less than two work days to get the serializer to a state where I can put it into practical use. It would have taken me the same amount of time to look for, assess and interface with an external library, which I would have had far less control over.

Share this post


Link to post
Share on other sites

Why make the on-disk layouts exactly match the in-memory layouts? What are you gaining by bulk-copying entire structures instead of individual fields? Why place the burden of managing and maintaining the subtleties of portability and implementation-defined behavior (which are easy to overlook and often difficult to debug) on the developer when they can be largely avoided? If you keep your on-disk layouts compact and express all data in terms of simple primitive types, then you can copy/assign one field at a time and the compiler will generate the correct code for you in all cases.

That being said, once you're at a point you can profile and determine if this is an actual bottleneck, then you can go back to exposing these responsibilities to the developer if the performance improvements are worth it. But I wouldn't start down this road initially, when you don't have any actual performance data to support the up-front developer cost and additional overhead.

For dealing with data schema that could be variable at run-time for whatever reason (i.e. savegames), I've found it's largely an issue of finding and using an appropriate model. JSON for instance works quite well for this because you can easily query and manipulate in-memory objects and utilize that meta-information to transform the data how your please, especially when working from a holistic view of the data. I've had to work with serialization systems that tried to manage versioned data using static structures and limited/partial access to the entire dataset, and it was an absolute nightmare.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this  

  • Advertisement
  • Advertisement
  • Popular Tags

  • Advertisement
  • Popular Now

  • Similar Content

    • By GytisDev
      Hello,
      without going into any details I am looking for any articles or blogs or advice about city building and RTS games in general. I tried to search for these on my own, but would like to see your input also. I want to make a very simple version of a game like Banished or Kingdoms and Castles,  where I would be able to place like two types of buildings, make farms and cut trees for resources while controlling a single worker. I have some problem understanding how these games works in the back-end: how various data can be stored about the map and objects, how grids works, implementing work system (like a little cube (human) walks to a tree and cuts it) and so on. I am also pretty confident in my programming capabilities for such a game. Sorry if I make any mistakes, English is not my native language.
      Thank you in advance.
    • By Ovicior
      Hey,
      So I'm currently working on a rogue-like top-down game that features melee combat. Getting basic weapon stats like power, weight, and range is not a problem. I am, however, having a problem with coming up with a flexible and dynamic system to allow me to quickly create unique effects for the weapons. I want to essentially create a sort of API that is called when appropriate and gives whatever information is necessary (For example, I could opt to use methods called OnPlayerHit() or IfPlayerBleeding() to implement behavior for each weapon). The issue is, I've never actually made a system as flexible as this.
      My current idea is to make a base abstract weapon class, and then have calls to all the methods when appropriate in there (OnPlayerHit() would be called whenever the player's health is subtracted from, for example). This would involve creating a sub-class for every weapon type and overriding each method to make sure the behavior works appropriately. This does not feel very efficient or clean at all. I was thinking of using interfaces to allow for the implementation of whatever "event" is needed (such as having an interface for OnPlayerAttack(), which would force the creation of a method that is called whenever the player attacks something).
       
      Here's a couple unique weapon ideas I have:
      Explosion sword: Create explosion in attack direction.
      Cold sword: Chance to freeze enemies when they are hit.
      Electric sword: On attack, electricity chains damage to nearby enemies.
       
      I'm basically trying to create a sort of API that'll allow me to easily inherit from a base weapon class and add additional behaviors somehow. One thing to know is that I'm on Unity, and swapping the weapon object's weapon component whenever the weapon changes is not at all a good idea. I need some way to contain all this varying data in one Unity component that can contain a Weapon field to hold all this data. Any ideas?
       
      I'm currently considering having a WeaponController class that can contain a Weapon class, which calls all the methods I use to create unique effects in the weapon (Such as OnPlayerAttack()) when appropriate.
    • By Vu Chi Thien
      Hi fellow game devs,
      First, I would like to apologize for the wall of text.
      As you may notice I have been digging in vehicle simulation for some times now through my clutch question posts. And thanks to the generous help of you guys, especially @CombatWombat I have finished my clutch model (Really CombatWombat you deserve much more than a post upvote, I would buy you a drink if I could ha ha). 
      Now the final piece in my vehicle physic model is the differential. For now I have an open-differential model working quite well by just outputting torque 50-50 to left and right wheel. Now I would like to implement a Limited Slip Differential. I have very limited knowledge about LSD, and what I know about LSD is through readings on racer.nl documentation, watching Youtube videos, and playing around with games like Assetto Corsa and Project Cars. So this is what I understand so far:
      - The LSD acts like an open-diff when there is no torque from engine applied to the input shaft of the diff. However, in clutch-type LSD there is still an amount of binding between the left and right wheel due to preload spring.
      - When there is torque to the input shaft (on power and off power in 2 ways LSD), in ramp LSD, the ramp will push the clutch patch together, creating binding force. The amount of binding force depends on the amount of clutch patch and ramp angle, so the diff will not completely locked up and there is still difference in wheel speed between left and right wheel, but when the locking force is enough the diff will lock.
      - There also something I'm not sure is the amount of torque ratio based on road resistance torque (rolling resistance I guess)., but since I cannot extract rolling resistance from the tire model I'm using (Unity wheelCollider), I think I would not use this approach. Instead I'm going to use the speed difference in left and right wheel, similar to torsen diff. Below is my rough model with the clutch type LSD:
      speedDiff = leftWheelSpeed - rightWheelSpeed; //torque to differential input shaft. //first treat the diff as an open diff with equal torque to both wheels inputTorque = gearBoxTorque * 0.5f; //then modify torque to each wheel based on wheel speed difference //the difference in torque depends on speed difference, throttleInput (on/off power) //amount of locking force wanted at different amount of speed difference, //and preload force //torque to left wheel leftWheelTorque = inputTorque - (speedDiff * preLoadForce + lockingForce * throttleInput); //torque to right wheel rightWheelTorque = inputTorque + (speedDiff * preLoadForce + lockingForce * throttleInput); I'm putting throttle input in because from what I've read the amount of locking also depends on the amount of throttle input (harder throttle -> higher  torque input -> stronger locking). The model is nowhere near good, so please jump in and correct me.
      Also I have a few questions:
      - In torsen/geared LSD, is it correct that the diff actually never lock but only split torque based on bias ratio, which also based on speed difference between wheels? And does the bias only happen when the speed difference reaches the ratio (say 2:1 or 3:1) and below that it will act like an open diff, which basically like an open diff with an if statement to switch state?
      - Is it correct that the amount of locking force in clutch LSD depends on amount of input torque? If so, what is the threshold of the input torque to "activate" the diff (start splitting torque)? How can I get the amount of torque bias ratio (in wheelTorque = inputTorque * biasRatio) based on the speed difference or rolling resistance at wheel?
      - Is the speed at the input shaft of the diff always equals to the average speed of 2 wheels ie (left + right) / 2?
      Please help me out with this. I haven't found any topic about this yet on gamedev, and this is my final piece of the puzzle. Thank you guys very very much.
    • By Estra
      Memory Trees is a PC game and Life+Farming simulation game. Harvest Moon and Rune Factory , the game will be quite big. I believe that this will take a long time to finish
      Looking for
      Programmer
      1 experience using Unity/C++
      2 have a portfolio of Programmer
      3 like RPG game ( Rune rune factory / zelda series / FF series )
      4 Have responsibility + Time Management
      and friendly easy working with others Programmer willing to use Skype for communication with team please E-mail me if you're interested
      Split %: Revenue share. We can discuss. Fully Funded servers and contents
      and friendly easy working with others willing to use Skype for communication with team please E-mail me if you're interested
      we can talk more detail in Estherfanworld@gmail.com Don't comment here
      Thank you so much for reading
      More about our game
      Memory Trees : forget me not

      Thank you so much for reading
      Ps.Please make sure that you have unity skill and Have responsibility + Time Management,
      because If not it will waste time not one but both of us
       

  • Advertisement