Analog Value Parity in Platform Fighters
Intro
As I make add features and improvements to the Frame1 firmware and the Glyph firmware (the latter based on Haybox), I've wanted to document them for the sake of other builders/modders. This one in particular is relevant to literally every type of controller for a platform fighter. It exists in other games, but we will focus on Smash Ultimate and Rivals 2 (because those are the two that needed to be "solved").
The Problem
Let's say you're using a rectangle-type controller (all buttons) for Ultimate. You have all your numbers set correctly for all your modifier buttons and angles when you use a GameCube adapter1. If you then plug into the console directly over USB in Switch Mode2 using the same exact values, your inputs will not work properly. Haybox and older versions of the Frame1 firmware solved this by simply multiplying it by a value (~1.3x) and calling it a day. In a game like Ultimate where the majority of analog stick zones are large and accessible, this should have been enough. I didn't dig into the specifics until a pikachu player noted that their double up-b input only worked in GC modes.
In order to gather the info I needed, I had to download a specific branch of an Ultimate training mod. By stepping through and recording every analog value vs the modded input display, I was able to get an idea of what was really happening.
The deadzones and maximum value of the analog values are different between the Switch and GCC modes, with GCC being the smaller of the two. Simply multiplying one by a number unfortunately wouldn't be enough.
GCC
Switch
value in code (+128) | "raw" output | Smash output |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
2 | 0 | 0 |
3 | 0 | 0 |
4 | 0 | 0 |
5 | 0 | 0 |
6 | 0 | 0 |
7 | 0 | 0 |
8 | 0 | 0 |
9 | 0 | 0 |
10 | 0 | 0 |
11 | 0 | 0 |
12 | 0 | 0 |
13 | 0 | 0 |
14 | 0 | 0 |
15 | 0.01785 | 2 |
16 | 0.03571 | 4 |
17 | 0.05356 | 6 |
18 | 0.07141 | 9 |
19 | 0.08927 | 11 |
20 | 0.10712 | 13 |
21 | 0.12497 | 16 |
22 | 0.14286 | 18 |
23 | 0.16071 | 20 |
24 | 0.17856 | 23 |
25 | 0.19642 | 25 |
26 | 0.21427 | 27 |
27 | 0.23212 | 30 |
28 | 0.24998 | 32 |
29 | 0.26783 | 34 |
30 | 0.28571 | 37 |
31 | 0.30357 | 39 |
32 | 0.32142 | 41 |
33 | 0.33927 | 43 |
34 | 0.35713 | 46 |
35 | 0.37498 | 48 |
36 | 0.39283 | 50 |
37 | 0.41069 | 53 |
38 | 0.42857 | 55 |
39 | 0.44642 | 57 |
40 | 0.46428 | 60 |
41 | 0.48213 | 62 |
42 | 0.49998 | 64 |
43 | 0.51784 | 67 |
44 | 0.53569 | 69 |
45 | 0.5354 | 71 |
46 | 0.57143 | 74 |
47 | 0.58928 | 76 |
48 | 0.60714 | 78 |
49 | 0.62499 | 80 |
50 | 0.64284 | 83 |
51 | 0.6607 | 85 |
52 | 0.67855 | 87 |
53 | 0.6964 | 90 |
54 | 0.71429 | 92 |
55 | 0.73214 | 94 |
56 | 0.74999 | 97 |
57 | 0.76785 | 99 |
58 | 0.7857 | 101 |
59 | 0.80355 | 104 |
60 | 0.82141 | 106 |
61 | 0.83926 | 108 |
62 | 0.85714 | 111 |
63 | 0.875 | 113 |
64 | 0.89285 | 115 |
65 | 0.9107 | 118 |
66 | 0.92856 | 120 |
67 | 0.94641 | 122 |
68 | 0.96426 | 124 |
69 | 0.98212 | 127 |
70 | 1 | 127 |
value in code (+128) | "raw" output | Smash output |
---|---|---|
0 | 0 | 0 |
1 | 0.00787 | 0 |
2 | 0.01575 | 0 |
3 | 0.02362 | 0 |
4 | 0.0315 | 0 |
5 | 0.03937 | 0 |
6 | 0.04724 | 0 |
7 | 0.05512 | 0 |
8 | 0.06299 | 0 |
9 | 0.07086 | 0 |
10 | 0.07874 | 0 |
11 | 0.08661 | 1 |
12 | 0.09449 | 2 |
13 | 0.10236 | 3 |
14 | 0.11023 | 4 |
15 | 0.11811 | 6 |
16 | 0.12598 | 7 |
17 | 0.13385 | 8 |
18 | 0.14173 | 9 |
19 | 0.1496 | 10 |
20 | 0.15748 | 11 |
21 | 0.16535 | 12 |
22 | 0.17322 | 13 |
23 | 0.1811 | 14 |
24 | 0.18897 | 15 |
25 | 0.19684 | 17 |
26 | 0.20472 | 18 |
27 | 0.21259 | 19 |
28 | 0.22047 | 20 |
29 | 0.22834 | 21 |
30 | 0.23621 | 22 |
31 | 0.24409 | 23 |
32 | 0.25196 | 24 |
33 | 0.25983 | 25 |
34 | 0.26771 | 27 |
35 | 0.27558 | 28 |
36 | 0.28346 | 29 |
37 | 0.29133 | 30 |
38 | 0.2992 | 31 |
39 | 0.30708 | 32 |
40 | 0.31495 | 33 |
41 | 0.32282 | 34 |
42 | 0.3307 | 35 |
43 | 0.33857 | 36 |
44 | 0.34645 | 38 |
45 | 0.35432 | 39 |
46 | 0.36219 | 40 |
47 | 0.37007 | 41 |
48 | 0.37794 | 42 |
49 | 0.38581 | 43 |
50 | 0.39369 | 44 |
51 | 0.40156 | 45 |
52 | 0.40944 | 46 |
53 | 0.41731 | 48 |
54 | 0.42518 | 49 |
55 | 0.43306 | 50 |
56 | 0.44093 | 51 |
57 | 0.44881 | 52 |
58 | 0.45668 | 53 |
59 | 0.46455 | 54 |
60 | 0.47243 | 55 |
61 | 0.4803 | 56 |
62 | 0.48817 | 57 |
63 | 0.49605 | 59 |
64 | 0.50392 | 60 |
65 | 0.5118 | 61 |
66 | 0.51967 | 62 |
67 | 0.52754 | 63 |
68 | 0.53542 | 64 |
69 | 0.54329 | 65 |
70 | 0.55116 | 66 |
71 | 0.55904 | 67 |
72 | 0.56691 | 69 |
73 | 0.57479 | 70 |
74 | 0.58266 | 71 |
75 | 0.59053 | 72 |
76 | 0.59841 | 73 |
77 | 0.60628 | 74 |
78 | 0.61415 | 75 |
79 | 0.62203 | 76 |
80 | 0.6299 | 77 |
81 | 0.63778 | 78 |
82 | 0.64565 | 80 |
83 | 0.65352 | 81 |
84 | 0.6614 | 82 |
85 | 0.66927 | 83 |
86 | 0.67714 | 84 |
87 | 0.68502 | 85 |
88 | 0.69289 | 86 |
89 | 0.70077 | 87 |
90 | 0.70864 | 88 |
91 | 0.71651 | 90 |
92 | 0.72439 | 91 |
93 | 0.73226 | 92 |
94 | 0.74013 | 93 |
95 | 0.74801 | 94 |
96 | 0.75588 | 95 |
97 | 0.76376 | 96 |
98 | 0.77163 | 97 |
99 | 0.7795 | 98 |
100 | 0.78738 | 99 |
101 | 0.79525 | 101 |
102 | 0.80313 | 102 |
103 | 0.811 | 103 |
104 | 0.81887 | 104 |
105 | 0.82675 | 105 |
106 | 0.83462 | 106 |
107 | 0.84249 | 107 |
108 | 0.85037 | 108 |
109 | 0.85824 | 109 |
110 | 0.86612 | 111 |
111 | 0.87399 | 112 |
112 | 0.88186 | 113 |
113 | 0.88974 | 114 |
114 | 0.89761 | 115 |
115 | 0.90548 | 116 |
116 | 0.91336 | 117 |
117 | 0.92123 | 118 |
118 | 0.92911 | 119 |
119 | 0.93698 | 120 |
120 | 0.94485 | 122 |
121 | 0.95273 | 123 |
122 | 0.9606 | 124 |
123 | 0.96847 | 125 |
124 | 0.97635 | 126 |
125 | 0.98422 | 127 |
126 | 0.9921 | 127 |
127 | 1 | 127 |
For reference, the sticks are 8-bit and the values range from 0-255 with ~128 being the center depending on the interface and game.
The game provides 2 values that indicate the internal "stick" position. The "raw" value, and some nice whole numbers (labeled "Smash Output"). The whole numbers were nicer to work with and seemed more reliable, so I went with those as the source of "truth". Some of these numbers are fully inaccessible, since the game's range (0-127) exceeds the accepted non-deadzone analog range of the controller (11-127 or 15-70). In switch mode, a "skip" occurs once every 11 or so values. In GCC mode, there's a skip between every value of 1-2 units. You can see these jumps in the graphs. In practice, it seems to have no affect on the relevant analog ranges the Frame1 uses.3
The Solution
There's probably a decently accurate one-line solution using the data I've gathered, but since we've already got all the data, we might as well use a lookup table.
By using the game engine's values as the source of truth, I created 2 arrays, one for each backend. By plugging in the desired value as the index, it gives you the value that will get you as close as possible to the desired value for the given backend.
The downside to this approach is that it requires all of the numbers in the Ultimate mode code to change. I'll get to migration and code changes in a moment, but if you need to convert your GCC coordinates to the new ones, here's a calculator.
New Value: 0
The only other thing that would have to change in the Haybox to allow this would be for the game mode variable to be passed to the game mode, so each individual game mode can scale or change the values for the backends as they see fit. I've already implemented this in the Glyph fork of Haybox but I also plan on making a PR so it can be added to the mainline repo.
What about Rivals 2?
Rivals 2 has pretty similar ranges between Xinput and GCC modes (110 vs 100). I've used the Xinput mode as the source of truth since it's the larger of the two. This means I just need one lookup table.
All values here are within 0.005 of the Xinput values, as measured by the game's input display.
Outro
First post here, so thank you for reading! While everything here works for my purposes, the Ult engine is admittedly not my area of expertise and Rivals 2 is brand-new. So if I got anything wrong or if something needs clarification feel free to @ me on Twitter or Bluesky
All of the data and code included in this article is free to use for anything you'd like
1 or a Frame1/Pico-Rectangle-based controller in Adapter Mode over USB.
2 "Switch Mode" in HayBox acts like a Pokken controller. Frame1 does the same in addition to a separate USB "Adapter Mode" that works on Switch. When I say "Switch Mode" I'm referring to the Pokken controller mode.
3 These values were captured by going straight in one direction or the other. Some otherwise inaccessible values can be accessed by going beyond the "rim". I do not have the time or the need to figure out how this calculated, but if any ult-engine-understanders want to chime in, please do