Kudos for thinking about a C# oriented solution. So many think in terms of limited scripts, when actually they’re writing full fledged C# applications, and scripts are just the source text files. The classes (AKA objects) are the real central concept to understand.
One minor point, though, is that the reason FixedUpdate is associated with physics operations is that the underlying means of calculating solutions to several real time physics problems must be performed on fixed timesteps. The engine isn’t capable of calculating solutions at variable time frames, by design. Interpolation can take two fixed time events and calculate a variable point between them, giving the illusion of a variable time oriented engine, but the fundamental operation is on fixed time steps.
The subject can run deep, and I’ll avoid going into all that occurs to me because there could be a book on just this subject.
However, before I suggest a design approach, I’d like to show an example of what I mean.
First, you’ve defined the essential issue. Input arrives at a time unsynchronized with when it must be consumed. Beyond that, however, there may be several different input methods for a particular control. A car might be controlled by keyboard arrows, a gamepad, touch, etc.
In keyboard control, the user may enjoy a configurable keyboard.
Input is provided by Unity from a platform independent framework, but an application could arrange for platform specific input from various devices, which may be taken at times independent of Update or FixedUpdate. An example might be using Windows interfaces to read gamepad input in an effort to be more responsive.
Input might come from bluetooth, the Internet, previously recorded game history.
Input may come from an AI agent, such that a character could be switched in and out of user control.
A review may suggest there ought to be an uber input manager. In a way, Unity’s input services are sort of a supervisory framework for taking input on various platforms.
What you’ve noticed is a rather obviously naive implementation of this notion that input is sensed at one time, and is consumed at another time, cluttering up the Update and FixedUpdate functions with a long list of 'if’s, many of which are redundant actions sensing various alternative input methods, and worse, state controls which change the interpretation of that an up arrow or some other key actually means based on the notion of some active object.
I’m going to try to simplify one professional style means of dealing with some of this. It may seem like considerable work compared to checking some bools, and it is reserved for more ambitious work, but is is also common of professional implementations.
First, I must introduce delegates, if they’re not familiar. A delegate is a variable which represents a function call. It is declared like this:
public delegate void func( int );
In this example a type is created, called func. It represents the type of a function that returns void and takes an integer. The signature of the function could be anything.
The delegate type can declare a variable, including a member of a class.
public func vfunc;
vfunc is a variable, and in this example is left dangerously null. vfunc represents a function call to any function that takes an int, returning void. At this point it is not assigned and can’t be used without causing an exception.
Now, let’s say I have a member function:
void aRealFunction( int n ) { myN = n; }
To assign vfunc, this works:
vfunc = aRealFunction;
At this point, vfunc is able to call aRealFunction with:
vfunc( 5 );
In all code where I state vfunc( 5 ), the real function that will be called is whatever vfunc is assigned to, and it can be reassigned at will at any time.
This technique can be used to create a class which can wrap the function call into the notion of a packaged message. This is an object which can be handed to a container, then later consumed by a function to call whatever function is wrapped in the message.
For example:
public class YourClass
{
public Queue< MsgCall > commandlist = new Queue< MsgCall >();
public delegate void func( int n );
void realFunc1( int n ) { .... }
void realFunc2( int n ) { .... }
.......
void realFuncN( int n ) { .... }
void AddMessage()
{
MsgCall c = new MsgCall();
c.vfunc = realFunc2;
c.v = 7;
commandlist.Enqueue( c );
}
void RunCommands()
{
while( commandlist.Count > 0 )
{
MsgCall c = commandlist.Dequeue();
c.Do();
}
}
}
class MsgCall
{
public YourClass.func vfunc;
int v = 0;
void Do() { vfunc( v ); }
}
This is illustrative and oversimplified psuedo code, also incomplete. I’m not trying to write a working solution, but to illustrate a concept.
There is a Queue, a list operating as a Queue. The AddMessage function can create a MsgCall, tell it a function to be called (later), and store parameters to be used when making that call, and store it in the Queue.
MsgCall could be configured to call any of the realFuncN functions.
Several such messages could be sent to the queue.
At some point, code would call RunCommands. This loop pops each entry from the Queue, executes them by calling the MsgCall’s Do function, which provides the stored parameter.
In this way, code can set up a number of functions to be executed at another time. This basic design has lots of uses, and a version of this is used by lots of message based systems (the older design uses numbers which then control a switch - like a bunch of if/elseif/elseif tests), but modern versions are based on the notion of “pointers to functions”, or as C# calls them, delegates, which represent function calls a data.
The approach is actually quite fast. If the realFuncN were trivial (and therefore fast), the Queue could rip through a million calls in a second, even on a phone. I have one that does over 50 million per second on typical modern Intel machines.
Now, this trivial example requires a more robust treatment. MsgCall should be made a generic, so it can work with any type of class, and any function signature I might require. To do that there must be an abstract base that Queue can store, which means a Queue could call many different types of functions on many different instantiations at once, as well as store several parameters, if required.
If return information is required, this is usually handled by calling a function that can consume the return at the time the Queue gets to the execution of the message, or…Queue a message that does that later.
In that code which reads input (Update at present), the typical input tests result in adding a MsgCall to the command queue, which is the entire decision (no further if/elseif checking for bools). It adds the command as if could call the function now, but since it can’t, it is set for being called when appropriate.
The FixedUpdate can consume this commandlist in a simple loop.
Between this introductory overview and the real product is a bit of a winding road through generics. It may seem questionable to create new objects for every command, and that’s a correct observation. The design may require a great many C# style ‘work arounds’ for counterparts from other languages that are higher performance, but there are myriad ways to interpret this concept to avoid performance issues.