#O2Jam v3.10 - Clone

1 messages · Page 1 of 1 (latest)

lusty lava
#

Intro

Download: https://cdn.discordapp.com/attachments/1297669929876590623/1297684481238503565/o2jam-clone-3.10-1.zip?ex=6716d25a&is=671580da&hm=de95756400f417b179fea7f1f0d89f2294175101a542093837ba5806cf6c5f5c&
Changelogs: #1297669929876590623 message

I believe I've been showing my game occasionally in the past within this server but I didn't have confidence to put this game on the showcase channel. But now I do! since the game is kind of complete now.

Hi folks,

I made a faithful remake of O2Jam, an online Korean rhythm game that launched in 2002 and was discontinued in 2012. There are poorly made mobile versions released before/after the termination, but they don't last and the game was pretty much considered dead.

This clone written in C++17 and currently runs offline based on the original game assets (even the music files are in the original format). The networking elements such as multiplayer rooms and other players are merely represented by dummy data. However, the core gameplay and the majority of game elements remain fully interactable.

If you're familiar with the original game or enjoy rhythm games in general, please give it a try!

Some handy windowing controls:

  • Alt+Enter: Switch between borderless fullscreen and windowed
  • Alt+↑: Switch between stretch and letter boxed fullscreen

Tested in MacOS as well, the older build even run on SteamDeck. I will probably release the MacOS build if someone interested

Notable Libraries

This game was built with a game framework (or engine?) that I write from scratch.
I 've managed to pull the framework out of the game and put it open source here:
https://github.com/SirusDoma/Genode

Project Background

This is a solo game development project that I’ve been working on for what feels like forever. The initial attempt dates back to around 2011-2012, followed by several complete rewrites. The initial commit in this repository was made in 2019 and currently sitting at 850 commits. I’ve gone through countless setbacks and struggles, not only due to technical challenges but also because of many high-level decisions. I would even say some of the struggles were mental. For example, it was hard for me to coming to terms with the fact that, no matter how much work I put into this project, it would never become legal or official due to legal issues, I may even put myself at legal risk. There are also other things, like the fact that this game will be running at low resolution, and among other things. This project has never been a top priority in my life which delayed the development even further, and I’ve given up on it so many times.

But earlier this year, a chinese company resurrected the game. Initially, I was excited, since the game had been dead for so long—at least, until I saw what they had done. I was completely disappointed by how the game was remade; it no longer retained the original game’s identity. Even without those expectations, it’s still a terrible rhythm game. And this isn’t their first attempt at remaking it—the first attempt was even worse. It became overwhelmingly negatively rated on Steam, making it the worst rhythm game that ever released on Steam.

While I was devastated, this situation gave me the motivation to finish this project, hoping it would soothe my disappointment and preserve my good memories from being tainted by their poor and greedy remake.

ionic yacht
#

Looks very cool!
It's the first time I hear of this game, but this looks like a pretty cool remake project!

ionic yacht
#

Is the repo public?

lusty lava
#

Github is such open space and I afraid putting it as open source will risk the entire project. I will make it open source should the situation "improve" (e.g the game become completely dead again once more)

ionic yacht
#

Understandable. I was just curious since you seem to have Linux support judging from the libraries you're using

#

It works through wine though, I tried it out a bit, it looks interesting, although I suck at these kind of piano roll games sfcrylaugh

lusty lava
#

I tried to compile the older version in SteamDeck and run on it (with vanilla SteamOS) and it does seems to run just fine, I have yet to try the latest one tho.

ionic yacht
#

The game crashes if I click the "First Step" button, haha

#

It's a page fault, unfortunately the backtrace seems useless

lusty lava
#

I think there's something missing with the asset files, it run just fine if I run the game via IDE with full assets but not when I copied the executable to with the distributable assets

ionic yacht
#

I like the overall feel, it truly feels like a 2000s korean online game, haha

lusty lava
#

Uhh seems I accidentally deleted the file when I trying to replace the file, I don't know how to upload it in original post but here the fixed version. Apparently there's path that is wrong in the code lol

#

Also another background: the most challenging technical problem was in fact not about the development (SFML / OpenGL) but actually reverse engineering the original client such as understanding game logics and running the game locally (remember, the game was originally an online game)

I would never dare to say the development was not challenging, but there's a time where I spent literally weeks to figure out something within the original client

ionic yacht
#

I played through the tutorial, I can confirm it works now

lusty lava
#

Nice, Thank you!

lusty lava
#

I've made an update to the game! Please read the changes below if you want to try the game just to be aware that the game now deals with Registry and INI file in system directory (which is default behavior of Win32 API of WritePrivateProfileStringA)
Nothing harmful but if you're not comfortable, stick with the older version

Release Notes

Changes, WIP and/or known limitations/bugs:

  • The game is still locked at 800x600 resolution using the native display refresh rate and cannot be changed.
  • Many Online-related UIs are still not functioning due to the client's offline nature.
  • Random music has not been implemented yet. Enabling and playing random music will cause undefined behavior/crashes.
  • The player profile and items are now adjustable via session.json for this offline demo.
  • You can now purchase items for free in this offline demo by simply adding items into the shopping bag and pressing the Buy button.
    The game will redirect you to the under construction page, but your items will still be added to your item bag regardless.
  • Equalizer is now implemented. However, the representation is a temporary implementation and different from that of the original client, and thus, it is subject to change.
    (Reversing this part will take some time since the original client used a separate thread to implement this)
  • Asset files have been compiled using the original package file format, but the individual files are still in non-binary format.
  • Now you can't run multiple instances of this game nor the original O2Jam client when running this client (vice-versa applies)
  • The game loads and stores the game config in the same manner and location as the (newer) original O2Jam client.
    This may cause config changes to be not saved properly without admin rights under common circumstances due to lack of access rights. (Changes will still be applied but it won't be persisted and will be lost when you close the game)
    • Registry: HKEY_LOCAL_MACHINE
      • EQ: Equalizer setting
      • 3D_Mode: 3D settings
      • 3DEffectLevel: 3D quality level settings (Unused for this version)
      • Mouse_Cursor: Mouse cursor settings
    • Config: C:\Windows\option.ini
      (Note: Windows may automatically redirect these locations to VirtualStore for compatibility purposes especially when it run with no admin rights)

Added shortcuts:

  • Press F8 in lobby or room to enable/disable Mouse Cursor
  • Press F9 in lobby or room to enable/disable Equalizer
pliant sage
#

Hello. It always makes me happy seeing people publish their framework after a really long and possibly tiring duration. I have only seen the open source framework. The code is really clean! I have a few questions, hope you don't mind:

  • you mentioned using json. Does that imply there is a save/load mechanism? It's interesting to see more.
  • why did you mark FontLoader and such final? I assume there is a good reason but I can't figure it out.
  • classes in Tween directory keeps a pointer to a Transformable or Colorable. nullptr does not seem like a valid pointer (pls correct me if I'm wrong), so maybe it's better to make them references?
lusty lava
#

Hi, thanks for showing interest!

  • json logic primarily resides in the game code and is used for two things: to describe a resource (or I call them resource metadata/definition) and to save a player profile (this is a temporary feature for the offline client)
    For resource metadata, it works like this: all my game objects, such as sprites, buttons, and animations, are serialized using JSON. The game loads this via the Gx::ResourceLoader mechanism (e.g., the game register loader using Gx::ResourceLoaderFactory::Register<Gx::Sprite, SpriteLoader>() during the start of the game). The sprite loader then implements the JSON loading, which is relatively straightforward, and there's no intervention from the framework. Checkout nlohmann/json for details if you're interested in serializing/deserializing json
  • I think it is accidental to mark FontLoader as final (probably due to unconsciously accepting IDE suggestions during refactoring). I will remove the final from the FontLoader.
  • The tween classes hold a pointer because I want to be able to instantiate the tween classes in an invalid state. This is certainly a somewhat borrowed pattern from SFML 2x (e.g., in the past, you could create sf::Sprite with the default constructor without specifying sf::Texture, now you can't do that in SFML 3 because the default constructor no longer exists). You need to supply a reference of the target (e.g Transformable/Colorable) in one of the other constructors in order to run the tween properly. I guess this will be changed in the future, and I will likely use reference (since tween cannot be rebound to another target)
#

I'm writing the tutorial under the project wiki here, but it is still pretty much incomplete:
https://github.com/SirusDoma/Genode/wiki

I can't promise anything, but I will try to publish some pages/tutorials on the wiki by this weekend.

pliant sage
#

I'm looking forward to seeing the wiki 😁, since I wanted to know more about your design choices and such, but it was hard to grasp without some examples.

  • I also used json (not json but jsoncpp) to handle save/load the scene graph in my game. I provided an overload (de)serialize free function to each component types that compose a node. Then a node contains the serialized array of its components, together with the node's info like parents and id. However I met a major drawback, sometimes serializing a type is quite repetitive, basically just call the overload function for each member and put the objects inside a bigger one. I wonder if you have the same problem and how did you resolve it.
  • about pointer/reference, it seems if you use raw references it's not possible to store Tween classes inside some std containers like std::vector. So I'm currently not sure if switching to them as I mentioned earlier is neccessarily a good thing.
    Anyways, congrats on marking a big milestone on your journey, and please keep up the good work!
lusty lava
#

Hi, I don't know if I can provide satisfying answer, here it is regardless:

  • I implemented resource loaders corresponding to each concrete node class (As I've said before, I have SpriteLoader, ButtonLoader, AnimationLoader, etc). The metadata has common properties that denote the concrete type of the node, and each node can have multiple children, which allows me to create a complex hierarchy. The JSON pretty much looks like this:
{
  "type": "Scene",
  "name": "STATE_PLANET",
  "require": {},
  "attributes": {
    "children": {
      "IDC_IMAGE_STATE_PLANET": "Interface/State/Planet/State_Planet.json",
      "IDC_ANIMATION_TOWER": "Interface/State/Planet/Planet_Tower.json",
      "IDC_CONTAINER_MUSIC_HALL": {
        "type": "UiContainer",
        "attributes": {
          "objects": {
            "IDC_RADIO_BEGINNER_01": "Interface/State/Planet/Planet_Btn_Beginner_01.json",
            "IDC_RADIO_BEGINNER_02": "Interface/State/Planet/Planet_Btn_Beginner_02.json",
            "IDC_RADIO_BEGINNER_03": "Interface/State/Planet/Planet_Btn_Beginner_03.json",
            "IDC_RADIO_HIGH_01": "Interface/State/Planet/Planet_Btn_High_01.json",
            "IDC_RADIO_INTERMEDIATE_01": "Interface/State/Planet/Planet_Btn_Intermediate_01.json",
            "IDC_RADIO_PREMIUM_01": "Interface/State/Planet/Planet_Btn_Premium_01.json"
          }
        }
      },
      "IDC_CHANNEL_BOARD": "Interface/State/Planet/Planet_ChannelBoard.json",
      "IDC_BUTTON_EXIT": "Interface/State/Planet/Planet_Btn_Exit.json"
    }
  }
}

The child could be a string that indicates the path of the json file or an object itself embedded there. Then, the deserialization itself is pretty straightforward: just iterate the children and resolve the common metadata properties (such as type and name) from the json path or object, then use the appropriate resource loader (Gx::ResourceLoader) to load that object. The only repetitive thing about this approach is that you have to map the node type with the resource type in a big switch-case/if-else (Gx::ResourceLoaderFactory somewhat slightly reduces the repetitiveness), and I have to implement multiple concrete resource loaders for each concrete node that I wish to deserialize.

Genode and my game do not have a mechanism to serialize data (apart from saving the player profile, which is quite straightforward and has no intervention from the framework), but I think it could be similarly implemented as the Gx::ResourceLoader (and its factory)

  • Regarding the second point, I'm not sure if we're talking about the same thing. I thought you were referring to the target in here or here, which certainly did not involve storing them in a vector at all. The Tween classes, however, can be stored in any way you like. Genode provide TaskContainer to conveniently manage tasks (tween classes are task) though and they're stored as a shared_ptr (in order to be able to return weak_ptr when you instantiate and run the task via Run function)
pliant sage
#

Ayoo thank you so much for such detailed answer 😊 I think C++ have some nice features to make the mapping name and type easier, like macros, templates and yeah inheritance.
Yeah I was reffering to the 4 classes in Tween directory. AFAIK putting references as a member of your class makes the said class not eligable to sensible copy and move operations, because references cannot be reassigned. That also recursively applies to classes that contains the class. But maybe that is what you intended. Either way it's okay I think.

#

Anyways good luck with your progress

lusty lava
#

No problem! Happy to see that someone is interested, I just wanted to address your little notes:
Macro could spread the repetitiveness into multiple places and make it somewhat look nicer, but I don't think it could entirely solve this problem with a couple of lines (or maybe it could?); it's just that I tend to avoid macros altogether in general. I heavily use the template in conjunction with the resource loader factory tho.

Lastly, about your last point, ah, yes, I got what you mean. I think using reference could work in conjunction with TaskContainer because the task (or, in other words, the tween) always stays inside the container via a shared pointer, which is the go-to way to deal with Task (not entirely sure tho if it will play nice with the TaskContainer, the container itself is not copyable but still movable). But I think it would be very inconvenient to use the task individually without the container, which I use it that way occasionally in my game. I could probably explore the std::reference_wrapper option, tho, but I'm not familiar enough with that thing, and I don't see why I need to use reference so badly, so I will leave it as it is for now. Thanks for letting me know!

lusty lava
#

Hi @pliant sage sorry for the ping, but I just wanted to let you know that I've written the documentation in the wiki here:
https://github.com/SirusDoma/Genode/wiki

The documents are probably littered with grammatical errors, so please bear with me. There are just so many things that I have to write orz