Ford Johnson
  • About
  • Projects
  • Blog

Matchmaking in Rocket League

Skill Rankings
Uncover the data behind Rocket League’s MMR and matchmaking system.
Published

September 21, 2024

As a longtime Rocket League player, I’ve always been fascinated by the ranking system behind the game. Using tools like BakkesMod and tracking websites, I’ve been able to monitor my MMR in real time, sparking my interest in how the system works. Rocket League, like many games, uses skill-based matchmaking to create balanced matches, driven by calculations that determine player ratings and matchups. My goal is to explore the data behind this system and understand how it shapes the competitive experience.

Matchmaking Rating (MMR) and Skill Rating

Most Rocket League players interested in their competitive ranking are familiar with the skill rating displayed on third-party websites or in BakkesMod. This skill rating is derived from the matchmaking rank (MMR), which remains hidden and is not easily visible to players.

\text{mmr} \approx \frac{\left(\text{skill rating} - 100\right)}{20}

\text{skill rating} \approx \left(20\text{mmr}\right) - 100

The MMR is the underlying numerical value that determines a player’s true skill level, influencing matchmaking and overall competitive performance in Rocket League. Both casual and competitive matchmaking in Rocket League utilize skill-based matchmaking, meaning that wins and losses directly affect your skill value, leading to increases or decreases in your ranking. To balance the time spent finding eligible opponents within selected regions and prevent long wait times, the game expands the allowable skill range when necessary. Different playlists and gamemodes will have their own MMR value.

Determining a Player’s Skill

Rocket League uses the SkillMu and SkillSigma to calculate a player’s MMR. This is similar to other Bayesian approaches but not the same.1

1 Reddit - How the Ranking System and Matchmaking works…

SkillMu (Mu)
The perceived skill level of a player, which increases with wins and decreases with losses. It starts at a value of 25 in unplayed playlists.
SkillSigma (Sigma)
The “uncertainty” value that decreases with each match, indicating the system’s confidence in a player’s skill. It starts at 8.333 in unplayed playlists and decreases as matches are played to a minimum of 2.5. A lower Sigma signifies greater certainty a player is at the correct skill level.

New players start with a SkillMu = 25, skill rating = 600 and SkillSigma = 8.333.

Matchmaking and Teams

In competitive Rocket League, players can choose between the following gamemodes:2 1v1, 2v2, 3v3.

2 Competitive and casual matchmaking use different weighting systems; this discussion will focus on competitive modes.

3 Reddit - Parties: How they affect matchmaking MMR

The matchmaking system employs a weighted average approach, using methods like the root mean square (RMS), to assess each team’s skill and ensure fair matchups against opponents.3

\text{TeamSkill}=\sqrt[n]{\frac{\mathrm{\text{mmr}}_{1}^{n}+\text{...}+\mathrm{\text{mmr}}_{x}^{n} }{x}}

party_matchmaking <- function(mmr_arr, n) {
  party_mmr <- round((sum(mmr_arr^n) / length(mmr_arr))^(1/n),0)
  return(paste0("Team's Skill Rating = ", party_mmr))
}

player_1_mmr = 600
player_2_mmr = 700
n = 15 # this is the value for competitive playlists

team_skill <- party_matchmaking(c(player_1_mmr, player_2_mmr), n)

team_skill
[1] "Team's Skill Rating = 673"

When a player’s skill reaches a certain threshold, the matchmaking system adjusts by basing the team’s skill level entirely on the high skilled player, even if they are partied with lower-skilled teammates. This ensures that the match remains competitive despite skill imbalances within the team.

Simulating Matches

Code
library(R6)

Rating <- R6Class("Rating",
  public = list(
    skill_mu = 25.0,
    skill_sigma = 8.333,
    sigma_sq = NULL,
    initialize = function(skill_mu = 25.0, skill_sigma = 8.333) {
      self$skill_mu <- skill_mu
      self$skill_sigma <- skill_sigma
      self$sigma_sq <- skill_sigma^2
    },
    get_mmr = function() {
      return(self$skill_mu)
    },
    get_skill_rating = function() {
      return(round((20 * self$skill_mu) + 100, 0))
    }
  )
)

Outcome <- list(WIN = "Win", LOSS = "Loss", DRAW = "Draw")

Rater <- R6Class("Rater",
  public = list(
    beta_sq = NULL,
    initialize = function(beta) {
      self$beta_sq <- beta^2
    },
    update_ratings = function(teams, ranks) {
      team_mu <- numeric(length(teams))
      team_sigma_sq <- numeric(length(teams))
      team_omega <- numeric(length(teams))
      team_delta <- numeric(length(teams))
      for (i in seq_along(teams)) {
        for (player in teams[[i]]) {
          team_mu[i] <- team_mu[i] + player$skill_mu
          team_sigma_sq[i] <- team_sigma_sq[i] + player$sigma_sq
        }
      }
      for (i in seq_along(teams)) {
        for (q in seq_along(teams)) {
          if (i == q) next
          c <- sqrt(team_sigma_sq[i] + team_sigma_sq[q] + 2.0 * self$beta_sq)
          e1 <- exp(team_mu[i] / c)
          e2 <- exp(team_mu[q] / c)
          piq <- e1 / (e1 + e2)
          pqi <- e2 / (e1 + e2)
          s <- if (ranks[q] > ranks[i]) 1.0 else if (ranks[q] == ranks[i]) 0.5 else 0.0
          delta <- (team_sigma_sq[i] / c) * (s - piq)
          gamma <- sqrt(team_sigma_sq[i]) / c
          eta <- gamma * (team_sigma_sq[i] / (c^2)) * piq * pqi
          team_omega[i] <- team_omega[i] + delta
          team_delta[i] <- team_delta[i] + eta
        }
      }
      result <- list()
      for (i in seq_along(teams)) {
        team_result <- list()
        for (player in teams[[i]]) {
          new_mu <- player$skill_mu + (player$sigma_sq / team_sigma_sq[i]) * team_omega[i]
          sigma_adj <- 1.0 - (player$sigma_sq / team_sigma_sq[i]) * team_delta[i]
          new_sigma <- max(sqrt(player$sigma_sq * max(sigma_adj, 0.0001)), 2.5)
          team_result[[length(team_result) + 1]] <- Rating$new(skill_mu = new_mu, skill_sigma = new_sigma)
        }
        result[[length(result) + 1]] <- team_result
      }
      return(result)
    },
    duel = function(p1, p2, outcome) {
      teams <- list(list(p1), list(p2))
      ranks <- if (outcome == Outcome$WIN) c(1, 2) else if (outcome == Outcome$LOSS) c(2, 1) else c(1, 1)
      res <- self$update_ratings(teams, ranks)
      return(list(res[[1]][[1]], res[[2]][[1]]))
    }
  )
)

Player <- R6Class("Player",
  public = list(
    name = NULL,
    rating = NULL,
    initialize = function(name) {
      self$name <- name
      self$rating <- Rating$new()
    }
  )
)

simulate_career <- function(rater, player_name, num_matches) {
  subject <- Player$new(player_name)
  opponents <- lapply(paste("Opponent", 1:10), function(n) {
    p <- Player$new(n)
    p$rating$skill_mu <- 25
    return(p)
  })
  cat(sprintf("Career Simulation: %s\n", player_name))
  cat(sprintf("Match %2d | Sigma: %5.3f | Skill Rating: %4d | Gain: --\n", 
              0, subject$rating$skill_sigma, subject$rating$get_skill_rating()))
  for (i in seq_len(num_matches)) {
    opp <- opponents[[sample(length(opponents), 1)]]
    old_sr <- subject$rating$get_skill_rating()
    result <- rater$duel(subject$rating, opp$rating, Outcome$WIN)
    subject$rating <- result[[1]]
    new_sr <- subject$rating$get_skill_rating()
    cat(sprintf("Match %2d | Sigma: %5.3f | Skill Rating: %4d | Gain: +%-2d\n", 
                i, subject$rating$skill_sigma, new_sr, new_sr - old_sr))
  }
}

rater <- Rater$new(beta = 25.0 / 3.0)
simulate_career(rater, "NewPlayer", 30)
Career Simulation: NewPlayer
Match  0 | Sigma: 8.333 | Skill Rating:  600 | Gain: --
Match  1 | Sigma: 8.202 | Skill Rating:  642 | Gain: +42
Match  2 | Sigma: 8.078 | Skill Rating:  680 | Gain: +38
Match  3 | Sigma: 7.961 | Skill Rating:  714 | Gain: +34
Match  4 | Sigma: 7.851 | Skill Rating:  746 | Gain: +32
Match  5 | Sigma: 7.749 | Skill Rating:  776 | Gain: +30
Match  6 | Sigma: 7.653 | Skill Rating:  803 | Gain: +27
Match  7 | Sigma: 7.563 | Skill Rating:  828 | Gain: +25
Match  8 | Sigma: 7.478 | Skill Rating:  851 | Gain: +23
Match  9 | Sigma: 7.399 | Skill Rating:  873 | Gain: +22
Match 10 | Sigma: 7.325 | Skill Rating:  893 | Gain: +20
Match 11 | Sigma: 7.255 | Skill Rating:  912 | Gain: +19
Match 12 | Sigma: 7.189 | Skill Rating:  930 | Gain: +18
Match 13 | Sigma: 7.127 | Skill Rating:  947 | Gain: +17
Match 14 | Sigma: 7.068 | Skill Rating:  963 | Gain: +16
Match 15 | Sigma: 7.012 | Skill Rating:  978 | Gain: +15
Match 16 | Sigma: 6.959 | Skill Rating:  993 | Gain: +15
Match 17 | Sigma: 6.909 | Skill Rating: 1006 | Gain: +13
Match 18 | Sigma: 6.862 | Skill Rating: 1019 | Gain: +13
Match 19 | Sigma: 6.816 | Skill Rating: 1032 | Gain: +13
Match 20 | Sigma: 6.773 | Skill Rating: 1044 | Gain: +12
Match 21 | Sigma: 6.731 | Skill Rating: 1055 | Gain: +11
Match 22 | Sigma: 6.691 | Skill Rating: 1066 | Gain: +11
Match 23 | Sigma: 6.653 | Skill Rating: 1077 | Gain: +11
Match 24 | Sigma: 6.617 | Skill Rating: 1087 | Gain: +10
Match 25 | Sigma: 6.582 | Skill Rating: 1097 | Gain: +10
Match 26 | Sigma: 6.548 | Skill Rating: 1106 | Gain: +9 
Match 27 | Sigma: 6.516 | Skill Rating: 1115 | Gain: +9 
Match 28 | Sigma: 6.484 | Skill Rating: 1124 | Gain: +9 
Match 29 | Sigma: 6.454 | Skill Rating: 1133 | Gain: +9 
Match 30 | Sigma: 6.425 | Skill Rating: 1141 | Gain: +8 
 
Cookie Preferences