1
\$\begingroup\$

I am trying to find a formula to use for my rhythm game's scoring. Basically there is a Guide entity and a Live entity. You control the Live entity and have to match it as closely as possible to a Guide entity in position and radius (both entities are circles drawn on HTML5 canvas).

Guide entities appear automatically and are pre-programmed in an array. For example: a beat trigger is at the 30 second mark in the song. A Guide entity is spawned at 30 - 2 seconds. Over the course of the two "buffer" seconds a visual cue lets the player know when to hit the trigger to spawn a Live entity (so there are two spare seconds for the player to orient themselves before the Live should be spawned). The Guide entity then starts to expand (increasing in radius) based on a pre-built duration setting. The player needs to hold down the Live entity key to expand their live entity along with the Guide entity, keeping them as close as possible.

Each Live entity has an attribute called tempPoints. When the Live circle is created, its tempPoints is set to 100 (the maximum, or "perfect" amount). On initialization of the Live entity (triggered by a key press on the user's side), I calculate the position offset from the Live entity to its matching Guide entity. Eg if guide.pos.x === 100 and live.pos.x === 110, live.pos.x is 10% off of guide.pos.x and tempPoints = 100 - 10 (same for pos.y)

That is done in the init function of the Live entity.

Here is where I'm having trouble:

The closer the Live entity's radius is to the Guide entity's radius as it expands (and then contracts) the more points the player should keep from their tempPoints. I think this would need to be done in the update function. The issue is that I don't know what formula to use. I originally did something similar with this as with the position points - calculate the percentage offset of the Live entity's radius from the Guide entity's radius and subtract this offset from the player's points. This would run every single frame and points plummeted into the negative within seconds. I then set up a timer to do this same thing but only run it every x seconds (eg 1 or 2), but am running into the same problem. It works in some instances, but seems dodgy because when the Live entity radius gets just a little too big or too small the offset becomes huge and removes a whole bunch of points. I've even tried dividing the offset by 10, 20, 50 to remove a smaller number of points, but can never get it to feel quite right. The point subtraction actually seems too minor until the offset is just a little too large, in which case it starts to look too drastic.

Is there something like an existing formula for this kind of point system? That is, points based on value A's proximity to value B (in this case the radius of your Live entity and the radius of the Guide entity), with both values continuously changing each frame as the guide and live entities expand and contract?

\$\endgroup\$
1
  • \$\begingroup\$ Interesting question. +1 for formatting and clear explanation. \$\endgroup\$ Commented Mar 7, 2014 at 17:42

4 Answers 4

2
\$\begingroup\$

I've never heard of this sort of abstract concept before so it's hard to imagine there would be an existing formula for it.

I would suggest removing the 'tempPoints' concept which makes it very hard to balance, and separating the process into 2 mathematical functions:

  1. Measuring how close the player is to the perfect result.
  2. Deciding how many points to award for being that closen.

This will let you tweak each of the 2 parts without needing to recalibrate the other significantly.

It looks like you're tracking 2 measures of quality: the initial position matching, and the radius matching over time. I'd be tempted just to treat them as 2 separate sources of scores, awarding between 0.0 and 0.5 for getting the position right and between 0.0 and 0.5 for matching the radiuses over time. In each case you need to decide just how much error a player can be allowed before they score zero points. Then anywhere between that point and perfection gets a score linearly interpolated between the 2 values. The radius one is slightly more complex because you need to track it over time, but really you can just take the average error over the duration of the entity.

Adding these values together will give you a value between 0.0 and 1.0. You then need to scale that up to award the final points to the player. If you wanted the player to get 100 points for a perfect action then you can multiply the value by 100. So a score of 0.25 on the previous system would yield 25 points. If players routinely were getting too few points then you can perform the scaling differently, eg. subtract it from 1, square it, subtract it from 1 again, then multiply by 100. Here, a score of 0.25 would yield 43.7 points.

If you really need the awarded points to be visible to the player and dropping during each action then you can perform the above calculations frame by frame, assuming zero error for each frame in the future. As each frame passes and the actual error is measured, the score awarded will also decrease until the duration expires.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks very much, I will try to implement something like this tonight and post an update asap with the results. \$\endgroup\$ Commented Feb 7, 2013 at 15:58
  • 1
    \$\begingroup\$ I agree with this answer. But I would also like to throw in an opinion from a game play perspective. In rythm games there can often be a visual/auditory disconnect, even if it's ever so slight, and audio cues are heavily focused on, to compensate for this and the possibilities of lag/visual update speed differences between frames I'd suggest a "forgiveness" area, nothing too big but at least something where no points would be lost as long as it's within a score of 0.9> maybe? Just my two cents. \$\endgroup\$
    – dennmat
    Commented Feb 8, 2013 at 2:37
  • \$\begingroup\$ I'm not sure I did it in the best way or exactly in the way you meant it, but I did end up taking your advice and making position and size award up to 0.5 respectively, then adding them and multiplying by 100 to get the final score at the end. I'm still in the process of experimenting and tweaking and it's not exactly right yet, but it already seems like a much better way to calculate my score. \$\endgroup\$ Commented Feb 11, 2013 at 12:20
1
\$\begingroup\$

As Kylotan suggests I would also separate distance computations and scoring over time from point manipulations. Having said that: the point manipulation is something that may require some trial and error.

Pseudocode:

// tweak c1
totalScore = initialScore = 100 / (1+c1*distance*distance)    

// other options 
// initialScore = 100 / (1+abs(c1*distance))
// initialScore = 100 / (1+sqrt(c1*distance))
// initialScore = 100 / sqrt(1+c1*distance*distance)    


while time is running and currentScore > 0
   // tweak c2
   factor = 1/(1+c2*distance*distance*)
   // choose functions similar to the approach above

   currentScore *= factor;

alternatively

while time is running and currentScore > 0
   // tweak c2
   intermediateScore = 1/(1+c2*distance*distance)
   // choose functions similar to the approach above

   currentScore = min(runningScore, average(intermediateScore, currentScore)

with average any of the typical mean functions like arithmetic, geometric or harmonic (http://en.wikipedia.org/wiki/Average).

Happy hacking :)

\$\endgroup\$
0
\$\begingroup\$

I am unsure if you have completely solved your problem spectralbat but in terms of the mathematical calulation for determining the distance you should use the following process.

  1. Define a vector facing from your center point to the point where you have dragged your live entity and determine its magnitude.
  2. Define a vector from your center point to the point that you are currently using to define your Guide entity and determine its magnitude.

How close the user is from having the correct radius of a circle (and therefore how close they are from the Guide entity) is simply a subtraction of the two magnitudes.

 // this is general pseudocode where abs is an absolute value function
 float DistanceBetween= abs(GuideVector.Magnitude- LiveVector.Magnitude)

In addition in determining the points value I would suggest that you create a value r which determines the precision that you want to measure. Often time the physical limitation of a screen will mean that the input may look exactly on the point but in reality it is not (depending upon the width of the line used to draw the circle and the size of the pixel on the screen).

You should also be aware that if you are determining you function for score based upon this distance it would be a smart idea to create a scaling of score rather than a function for the score. If you are using a function similar to this:

 Score= ScoreMultiplier (1/DistanceBetween)

Then your function can in theory go to infinity if you get within a pixel (in addition if the size of a pixel is the limitation then a different score can be determined based uon the resolution of the screen since the pixel size can be different from one screen to the other). In order to keep your score from having this trend a lot of programmers will create a relationship where they either cap the highest score that can be achieved or to create a table look up that says if you are within this set of distances the score will increase at this value per second.

I hope this helps.

\$\endgroup\$
0
\$\begingroup\$

(hooo you there ! welcome :-) i deleted this post before because i was unhappy with it by maybe you can make something out of it, so here it is again. )

If i may, there's a fundamental point missing in your analysis : your game is time-dependant, while your formula is not time dependant.
You need to handle time, both for a right scoring, and to enable your game to work on several devices having differents FPS
(the game framework you use rely, like many others, on requestAnimation frame).

That's why, i think, you get your : ' It works in some instances'.
So the tempPoint you mention is should be relative to the guide duration, and, on each update, you should integrate other time, not doing fixed add/substract that will randomly depends on duration/player accuracy.
So, to integrate, you need the 'dt', the duration of your frame (look for **.system.tick ) How to integrate ? When i see i'm risking a headacke, i normalize : let us say you have, at each moment in time, a normalized measure of accuracy , 'playAcc' : 0 means player is outside, 1.0 means player is fully on target.
you will have :
so do something like :
- on a new guide you store guide duration into currGuideDuration, and set currGuidePerfectScore to the value you want the player to win for a perfect (always 100 if i got it well). - in each update, you :
- compute normalized accuray : playAcc.
- currGuideScoreDelta = playAcc * dt / currGuideDuration
- score += currGuideScoreDelta * currGuidePerfectScore i think it is quite easy to see that with a constant accuracy of 1.0 during currGuideDuration, the score increase will be of currGuidePerfectScore.

(Rq : another way of doing which i find more fun, is to have bouncing and growing current score displayed in the Guide, and then score updated only at the death of the guide, the score also bouncing and growing with more or less hapyness depending on score increase : have a 'currGuideScore' that you increase by currGuidePerfectScore*currGuideScoreDelta each update, and then only at the end you increase the score by that currGuideScore, having the score bounce with more or less hapinness or like. )

But how to compute the normalized accuracy you would ask ? Kylotan suggest a way, i prefer the normalized measure of the area of intersection of the two circles. But since the formula is a headacke (search google, you'll see...), i would compute an approximation :
1) check if circles are away (distance > radius1 + radius2) : playAcc = 0.
2) if not, then compute nmaiibbobb : the normalized mean of ( the area of intersection of the inner-bounding box) and (the area of intersection of the outer bounding box) of the two circles. Excuse me for not coding this right now... :-)
Just two tips :
a) : the length of the inner bounding square of a circle is radius/sqrt(2)
since sqrt(2) is a constant, compute it once in init().
b) 'normalizing' means, here, dividing by the Guide's bbbox area. So we can easily see that for perfectly matching circles, nmaiibbobb=1.0. If now Live entity is containing the Guide, only the guide'area will count and, yes, we will have 1.0. The player will loose points (=increase less than he could have) later, when its Live becomes smaller than Guide, and nmaiibbobb <1.0. So the late player will go haaaaaaaa (joy) then hooooo (sadness) when playing.

I wait your game with impatience !!!

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .