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
Refto 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
GameandGame_ImplwithGameplay/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
-Sceneinstead of current convention-Screen, since-Screengives me impression that it's a GUI-related class. - Put
.hppheader files alongside.cppfiles, not under/include.
- PImpl pattern will not be used.
-
GameplaySceneconsists 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_gameplayfield for a component is frowned upon. It will encourage devs to write mangled code. - Also, I specifically want for
PlaybackComponentto 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
GameplaySceneis 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 LuaComponentDebugHUDComponent-
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
newanddeleteas 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
-Screenclasses into-Scenefor 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-refactorbranch. - May rename
ScoringtoScoringComponent. - First PR will not separate other parts of
Gameinto components.- Current multiplayer, replay, practice-mode, and Lua codes will still reside under
GameplayScene. - If separating
PlaybackComponentalone 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_fieldinGame_Implto move under the componentFooComponent. - Create a temporary getter
Field& FooComponent::TEMP_field() { return m_field; }. - Replace all occurences of
m_fieldinGame_Implintom_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
FooComponentsuch 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 -
loaderfor 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