- Home /
Counting points constantly, without framerate dependency
Hi.
I'm making a simple snowboard game, THPS/Ollie Ollie style, and implemented two ways of getting points.
1. By spinning character in the air
2. By grinding on rails
Here's my implementation of those mechanics:
Scoring for spins:
void Update()
{
ScoreForSpins();
}
private void ScoreForSpins()
{
if (!triggerEffects.gameOver && !triggerEffects.grinding)
{
zAxis = (int)transform.eulerAngles.z;
if (zAxis >= expectedzValue - 5 && zAxis <= expectedzValue + 5)
{
scoreForSpins += 50;
if (expectedzValue == 180)
expectedzValue = 0;
else
expectedzValue = 180;
}
}
}
Scoring by grinding
public IEnumerator ScoreGrinding()
{
while (triggerEffects.grinding)
{
yield return new WaitForSeconds(0.02f);
grindScore += 2;
}
}
Generally this works fine, however my issue is, that those solutions are framerate dependant.
On my PC it works as intended, however on my girlfriend's slower laptop,
counting score for spinning character often misses the
zAxis >= expectedzValue - 5 && zAxis <= expectedzValue + 5 range,
and the 50+ score isn't added
I think it's because of lower framerate, sometimes the frames when this would be checked in Update() are being skipped.
For grinding there's a separate issue.
On PC running the game with lower framerate, the
yield return new WaitForSeconds(0.02f);
is checked less ofter, because the time between frames is higher then 0.02f, which means getting less points for the same time of grinding on the rail
Of course that is a problem, because every player should be able to get the same amount of points for the grind, no matter their framerate
For the second issue propably setting the WaitForSeconds to higher value, like 0.1f, would solve it, however I would really like to stick to the frantic counting system ala THPS.
Is there any smarter solutions for those issues?
Thanks!
Answer by MickyX · May 07 at 11:57 AM
I would suggest using FixedUpdate in place of Update as FixedUpdate isn't framerate dependant and see if that works for you
Answer by KoRnish7 · May 07 at 01:09 PM
Unfortunately, putting it in FixedUpdate makes it even worse, 50 points that should be added for 180 rotation misses even more often then in Update
(I suspect that while FixedUpdate is frame independent, it may actually run less frequently then Update(), and I need thick check to be performed all the time).
It does indeed run less frequently. There is a setting to change it, but if it doesn't work for you I wouldn't suggest doing that.
Try calculating the points based on Time rather than an iteration of Update , multiplying a score by Time.DeltaTime should be the same regardless of frame rate, as it is the time passed since the last frame.
Are you talking about the scoring for spins, or for grinding?
The former I believe has to be checked as frequent as possible. It adds points when the transform.eulerAngles.z; of player character is between 5 points to the left or right of 180 degree opposite.
Ideally it would just check if transform.eulerAngles.z; is 180 degree opposite of the start value, and then add a point.
however even on my PC I've noticed that then most of the time the check wouldn't activate, so I've added 5 points of error limit to both sides.
I could add more, sure, to compensate for slower framerate, and therefore less frequent checks on slower PCs, however then earning the points is getting to easy and less precise.
I would need fixed checks more frequent then framerate (in case framerate gets bogged down), however I don't know if that's even possible :/.
As for the score counting for grinding, it's already being count by time, and not by frames, since its in coroutine with yield return new WaitForSeconds(0.02f);.
The problem is, again, that WaitForSeconds only works consistently for the value provided as argument, if that value is higher then time between frames.
If time between frames gets higher then 0.02f, then the commands in coroutine are getting call less frequently, and we're ending up with an unreliable scoring system :/
Looking at ScoreForSpins. Can you try some thing like the below this would mean it calculates the score as closely as possible to the target rotation.
switch (expectedzValue)
{
case 0:
if (zAxis >= 0 && zAxis < 180)
{
scoreForSpins += 50;
expectedzValue = 180;
}
break;
case 180:
if (zAxis >= 180)
{
scoreForSpins += 50;
expectedzValue = 0;
}
break;
}
Answer by Eno-Khaon · May 07 at 10:35 PM
Yeah, there's no denying it. Both of these are thoroughly framerate-dependent in different manners. But, you're aware of it, so let's just jump right into this.
First, let's look at rotation:
Rather than strictly relying on your rotation in the form of EulerAngles, count up your rotation yourself, then translate that result back into rotation as desired.
// Example: Rotation input ranges from [-1 to 1] as a
// clockwise/counterclockwise rotation
if(!Mathf.Approximately(rotationInput, 0.0f)) // If not zero
{
// currentRotation should be zeroed out while on the ground
// rotationSpeed would be degrees per second rotation
float frameRotation = rotationInput * rotationSpeed * Time.deltaTime;
currentRotation += frameRotation ;
transform.rotation = Quaternion.AngleAxis(frameRotation, Vector3.forward) * transform.rotation;
// example: rotationThreshold is 180-degree rotations to gain points
if(Mathf.Abs(currentRotation) <= -rotationThreshold)
{
currentRotation -= rotationThreshold * Mathf.Sign(currentRotation);
activeScore += 50; // using your 50-point example for spins
}
}
Now, for grinding, you should probably give score based on distance traveled. In theory, if you were moving an inch per hour, should you still get the same score as you would if you traveled across the entire rail?
Ideally, the score for the current grind should also be stored as a float to ensure that it can be incremented fractionally, rather than only as integer increments. It can just be added to the main score in integer increments, however.
grindScore += Mathf.Abs(distanceTraveledThisFrame);
score += Mathf.FloorToInt(grindScore);
grindScore = Mathf.Repeat(grindScore, 1.0f); // Remove whole number value
Thanks for the detailed answer and proposed solution Eno-Khaon!
Not gonna lie, since I'm a beginner to coding, and completely worthless at math and physics, your solution eludes me at the moment. If you've got a moment to spare, I'll be thankful if you could provide some additional explanation :)
For the spinning solution:
1. What type should rotationInput be, and where should I get it from?
2. Same for rotationSpeed, currentRotation, and frameRotation... are those all values that I should somehow read's from player character's transform?
If they are, how does this make it framerate independent, compared to reading the EulerAnglers... you still have to read and calculate values every frame, right?
Solution for grinding seems much simpler, however I don't know how I can read distance traveled this frame.
Sorry if questions seems stupid, as I said, I'm new at this.
Everyone's new at some point. It's nothing to worry over. Anyway...
1: "rotationInput" in my example is based on however you're providing input. It could be...
float inputRotation;
// ...
inputRotation = Input.GetAxis("Horizontal");
// or...
inputRotation = 0.0f;
if(Input.GetKey(rotateLeftKeyCode))
{
inputRotation -= 1.0f;
}
if(Input.GetKey(rotateRightKeyCode))
{
inputRotation += 1.0f;
}
... among other options, such as the newer input system.
The point would be to have Input available in a [-1 to 1] range for ease of use.
2: "rotationSpeed" would be the number of degrees rotated per second, since I assume you're managing that in some manner, otherwise you would rotate VERY slowly.
"currentRotation" is the total sum of how far you've rotated continuously in the air since you last left the ground (as a typical metric for that sort of stunt system), minus increments to cut off (such as 180 degrees). That way, you can't just wiggle forward and backward and still have it count as a frontflip/backflip. You have to make full portions of rotation, but would still be allowed to change direction partway through.
Then, "frameRotation" would just be the amount of change applied in the most recent frame, as the combination of elements and framerate-independent scaling with Time.deltaTime.
float frameRotation = rotationInput * rotationSpeed * Time.deltaTime;
Finally, distance traveled per frame while grinding could be measured in a number of ways, depending on your intent. If you would want to factor in only horizontal distance, you could snag just the x-axis element. Otherwise, you could snag the movement vector's magnitude.
Since I don't know whether you're utilizing physics, I've just been offering the difference-driven variant(s):
Vector3 lastPosition;
// ...
void Update()
{
// Distance traveled in the last frame
Vector3 positionDelta = transform.position - lastPosition;
float grindDistance = positionDelta.x; // If you want only X-axis
float grindDistance = positionDelta.magnitude; // For all axes
// Store the current frame's position to use on the next
lastPosition = transform.position;
}
Your answer
Follow this Question
Related Questions
Multiple Cars not working 1 Answer
Distribute terrain in zones 3 Answers
3d listener position only updates once 0 Answers
Show message when score achieved 1 Answer
Change Bool When Audio Clip Ends C# 1 Answer