GameRefactoring - 123jimin/unnamed-sdvx-clone GitHub Wiki
The refactoring is currently being done under the 202204-game-refactor
branch.
The scope of the refactoring is ones related to Game
and Game_Impl
class, not the entire game.
Current Game.cpp
is over 3500-LoC monolith code, including logics for Lua bidings, particle system, rendering, practice mode, multiplayer, and replays.
LoC itself is not a significant problem here, but multiple hundreds-of-lines of functions with various purposes put in a large PImpl class makes the code confusing to navigate - at least for me who have seen the code for several years.
I have many future suggestions for features in USC, and certainly other devs have ambitious plans on USC too.
- HitStat rendering on each note
- In-game chart editing and live reloading
- Introducing mouse-navigatable UI for the practice mode
- More sophiscated calibration mode, including microphone-based calibrations
- Make charts keep playing after audio has ended
- Make audio keep playing after charts have ended
However, I can picture that while adding these to current codebase is definitely feasible, it will be likely done by adding sprinkles of if-elses inside random functions of Game_Impl
, and it will only make Game.cpp
more complex and unreadable.
There have been discussions on refactoring/rewriting the codebase, such as this one in '18 or this one in '19, but they seem to be inactice now. I suspect that the reason behind these discussion being stuck is a lack of benefit vs. cost:
- While replacing raw pointers and
Ref
to STL smart pointers is certainly useful, itself is little more useful than removing "a few crashes"- There are some lifetime-related crash issues but they don't impact a lot of players now.
- Rewriting the code including implementing various specific features (multiplayer, practice mode, replay, calibration, ...) is quite tedious, and will cause a lot of regressions.
I propose to refactor Game.cpp
, with a method which can be done incrementally, and would result in cleaner and more managable code.
- Replace
Game
andGame_Impl
withGameplay/GameplayScene
- PImpl pattern will not be used.
- I think that doing so will make the codebase more navigatable using the header file.
- Instead, I believe that component-based structure will remove much of the implementation details in the header file.
- I chose
-Scene
instead of current convention-Screen
, since-Screen
gives me impression that it's a GUI-related class. - Put
.hpp
header files alongside.cpp
files, not under/include
.
- PImpl pattern will not be used.
-
GameplayScene
consists of many components with different, mostly orthogonal functions. (Held bystd::unique_ptr
)- Optimally communication between different components are only done in
GameplayScene
, but there might be some exceptions. - Even in those exceptions, explictly specifying dependent components as
const Component&
function args is preferred.- e.g. defining
GameplayScene& m_gameplay
field for a component is frowned upon. It will encourage devs to write mangled code. - Also, I specifically want for
PlaybackComponent
to work independent of other components (as much as possible).
- e.g. defining
- Delegates will be extensively used to send 'signals' between components. (Any opinion on this?)
-
PlaybackComponent
: Codes related to playback of the chart- Consider it as a
<video/>
for a chart. - Consists of
BeatmapPlayback
,AudioPlayback
,Camera
,Track
, particle system, ... - Example functions:
Render(...)
,SetStart(...)
,Pause()
,Play()
,GetCurrMapTime()
,GetCurrMapObjects()
, ... - It must not manage player inputs / autoplay status / ... directly, but instead be fed by function arguments.
- Consider it as a
-
InputComponent
: Codes related to input handling- It might not be necessary (putting those logics in
GameplayScene
is fine by me)
- It might not be necessary (putting those logics in
-
ScoringComponent
: Codes related to gauge, score, and judgement management- Basically the old
Scoring
- Basically the old
-
FGBGComponent
: for rendering foreground/background LuaComponent
DebugHUDComponent
-
MultiplayerComponent
,ReplayComponent
,PracticeComponent
, ...
- Optimally communication between different components are only done in
- Make new and refactored C++ code modern
- Use
std::array
,std::vector
(orVector
?) for lists - Use
std::unique_ptr
,std::optional
, and references instead of raw pointers and bool flags.- Don't think that a shared pointer is necessary (unless old code requires using it)
- Custom destructors must be as minimal as possible. Reduce usage of
new
anddelete
as much as possible.
- Use
- Implementing new features (it should be done separatly)
- Refactoring with future plans in mind
- Premature refactoring almost always goes wrong.
- Refactor further during implementing new features.
- This refactoring will make those further refactorings easier.
- Refactoring codes not directly related to
Game
- Renaming other
-Screen
classes into-Scene
for now
Each item will be a separate PR.
- Move playback-related codes into
PlaybackComponent
, and move rest of the codes intoGameplayScene
.- It's currently being done on the
202204-game-refactor
branch. - May rename
Scoring
toScoringComponent
. - First PR will not separate other parts of
Game
into components.- Current multiplayer, replay, practice-mode, and Lua codes will still reside under
GameplayScene
. - If separating
PlaybackComponent
alone will be PR-worthy, then it will prove that further refactoring is beneficial...
- Current multiplayer, replay, practice-mode, and Lua codes will still reside under
- It's currently being done on the
- Separate Lua-related codes into
LuaComponent
, and the debug HUD intoDebugHUDComponent
. - Separate other components.
Separating each component will be done in a simple manner, which will reduce regressions and also can be tested progressively.
- Choose a field
Field m_field
inGame_Impl
to move under the componentFooComponent
. - Create a temporary getter
Field& FooComponent::TEMP_field() { return m_field; }
. - Replace all occurences of
m_field
inGame_Impl
intom_fooComponent->TEMP_field()
. - Repeat 1-3 until a logical piece of code is found which only deals with
m_fooComponent
. Move that piece of code intoFooComponent
. - If a logical piece of code mainly deals with
m_fooComponent
, but it requires fields from some other external codes, then create a function that receives temporary arguments prefixedTEMP_
. Deal with those arguments later. - After all relevant fields are moved, improve the public interface of
FooComponent
such that allTEMP_
usages can be removed.
-
m_hispeed
-
cMod
-
cModSpeed
-
m_speedMod
-
m_modSpeed
-
m_hideLane
-
m_beatmap
- old
m_playback
(m_beatmapPlayback
) -
m_audioPlayback
- combined audio offset
-
m_track
-
m_lastMapTime
-
m_endTime
-
m_camera
-
m_currentTiming
-
m_fxVolume
-
m_slamVolume
-
m_slamSample
-
m_clickSamples
-
m_fxSamples
-
m_rollIntensity
-
m_manualTiltEnabled
-
particleMaterial
-
basicParticleTexture
-
m_particleSystem
-
m_laserFollowEmitters
-
m_holdEmitters
-
m_showCover
-
m_hiddenObjects
-
m_permanentlyHiddenObjects
-
loader
for async-loadingTrack
Game.cpp
still looks kinda messy and the API needs to be improved.
- Normal gameplay
- Autoplay
- Normal play
- Pause
- Skip forward/backwards
- Speed changes
- XMod
- MMod
- CMod
- Mod changes mid play
- Multiplayer
- Practice mode
- Chart scrolling and navigation
- Play once
- Play with missions
- Play with "max # of measures to rewind" option
- Play with speed-increase modifiers
- Challenge
- Replay
- Debug HUD