Skip to content

Open Hexagon 2.1.7 pack that uses a custom engine on top of Pseudogame for next-generation concepts

Notifications You must be signed in to change notification settings

Morxemplum/demo_tapes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

1df6106 · Dec 17, 2025

History

5 Commits
Dec 15, 2025
Dec 15, 2025
Dec 15, 2025
Dec 17, 2025
Dec 15, 2025
Dec 15, 2025
Dec 15, 2025
Dec 15, 2025

Repository files navigation

Demo Tapes

Cover art

Demo Tapes was meant to be a pack written for Open Hexagon 2.1.7. It was my first original content that I was making in a long time. Demo Tapes also was meant to serve as a blueprint and working proof of concept that would lay out a lot of the ground work for a future Lua-based engine in Baumm's rewrite of the game, utilizing the pseudogame library and building a custom engine on top of it to showcase concepts not possible to achieve in the current engine (unless you do what I do right here, or heavily abuse shaders). The concepts were meant to add new features and tools to easily be utilized by future pack developers.

If you downloaded this from the Steam Workshop, then I'm sure you've already some of the basics regarding this pack. If you are reading this on my GitHub, then a couple things to know:

  • This pack is unfinished. I hid all of the levels I've thought about, but don't have any implementation. That's all levels except for one. I talk later about why this pack is unfinished
  • You'll need the pseudogame dependency. My custom engine builds on top of the pseudogame library to handle emulated rendering logic.
  • Please feel free to read the Lua code. While my custom engine implementation consists of many moving parts, you may find a couple of modules useful to incorporate into your own level packs.

Custom Engine

All of the modules for the custom engine

One of the main motivations for me to make this pack was the opportunity to use the power of pseudogame to basically create my own Open Hexagon engine. Take a look into my level script and you will quickly realize that a level script in Demo Tapes looks nothing like a regular level script. This is on purpose.

Some of this has to do with interfacing with Pseudogame and having to mute some of the default engine behaviors for my custom engine. Otherwise, a lot of this involves trying to redesign the engine and scripting in a way that would try to make things simpler for new pack developers, while being able to tap into incredibly powerful customization for experienced pack developers.

Some of these concepts aren't entirely new. They have been made individually by other pack developers before. However, my engine was meant to take all of these concepts and have them coalesce into one engine.

Namespaced Engine Functions

In Open Hexagon 2, you can easily identify engine Lua functions as they are prefixed (prefix_functionName(args)). While this made it incredibly easy to categorize different engine functions, it is unintuitive for new pack developers to try and memorize the functions. On top of this, some of the functions were put in categories that didn't make sense.

One proposal for a new engine is that these prefixed functions would be replaced with a name spacing convention (i.e. u_inMenu would become Utility.in_menu, a_setMusicSeconds becomes Audio.music.set_seconds, u_haltTime moves from utilities to level functions through Level.pause_timer).

I created a non-destructive patch that accomplishes this with ALL game functions. The bindings are linked through callable references, so libraries are able to override game functions and the bindings will still work. These bindings are used throughout my custom engine implementation.

Everything is a "Custom Wall", and More

In Open Hexagon, many of the walls perform a simple behavior: move towards the pivot and try to kill the player. In Open Hexagon 2, a new type of wall was created to end all future wall implementations: custom walls. Custom walls are programmable in Lua, allowing pack developers to make walls that did a whole lot more.

There was a cost: Unlike standard walls, custom walls are objects. This had a huge performance cost when pack developers tried making levels that utilized hundreds, if not thousands of custom walls.

Although I cannot fix this issue without patching the engine itself, I have come up with a new wall engine that was designed with performance in mind through a data-oriented framework. In my engine, every wall is considered a "custom wall", where you need to program what they do. That being said, I have programmed functionality that recreates the behavior of "standard walls" through wall presets and a brand new pattern construction and management system. That way, you don't have to completely reinvent the wheel.

Walls that adapt

Don't think that walls are static in nature. I have laid out the framework for an event-listener system that can be performed on walls. With this system, walls can listen and adapt to changes in their environment. There was only one type of event that I created in the time I was working on Demo Tapes, and that was sides changing. This concept was fairly new and I wanted to flesh this out considerably to account for distance triggers, collisions with other walls, etc. Unfortunately, I never got to expand this out.

There was one other event, which is how walls will spawn fading in, instead of just appearing. This was a more hardcoded behavior though, as I didn't want this changed.

An Improved Pattern Manager with Spatial Construction

On top of walls being completely redone, I completely redid how patterns are created and managed in a level. The current Open Hexagon pattern manager is incredibly static and doesn't have much configurability, unless you were willing to modify the walls in the patterns themselves. My pattern manager, with the combination of dedicated functions for spawning a wall, treats walls as a building block. That way, instead of having to harshly focus on the small details, you can easily look at things from the big picture. My pattern manager also has configurable defaults that can be changed by pack developers to easily affect construction and behavior of existing patterns on a higher level, making pattern tinkering much more approachable to new pack developers.

Traditionally, patterns in Open Hexagon are constructed temporally. You would have to spawn walls based on waiting a specific delay. However, there are a few major problems with this approach:

  1. Delay values are unintuitive. By default, the unit of time used in delays is "frames". Not ticks, but frames. This had to do with carrying the baggage of back when 60 FPS was the de facto refresh rate back in 2012, when Open Hexagon got it's final releases in 1.92 and 2 was in release candidate stages pre-Steam. When Open Hexagon switched to using a tick-based system in the Steam release, delay values did not convert for the sake of reverse compatibility. There are alternative delay functions that allow you to use seconds, but seconds are not a unit that properly captures the precision of pattern construction (why yes, I would like to wait 1.19583333333 seconds for my next barrage).
  2. Delays do not adjust well to differing speeds. Sure, you can figure out good spacing for a pattern at a speed multiplier of 1. However, that same pattern will not have the same spacing if you were to up the speed to 3 or 4. This problem compounds when considering that some (well known) patterns contain walls whose thickness is set based on the spacing of patterns. Vittorio Romeo did see this problem early on and "solved" this problem by creating dedicated functions that would try and calculate delay and thickness values based on the speed multiplier, called getPerfectDelay and getPerfectThickness respectively. There's just one problem...
  3. There is no such thing as a "perfect" delay or thickness. When the function names say they are, it's a bunch of bullshit. The existence of these functions took problem #1 and made it worse. What's hilarious is that the default functions don't even consider the "delay multiplier" level attribute. You have to actually use separate functions called getPerfectDelayDM and getPerfectThicknessDM to factor in the delay multiplier. This just makes the delay multiplier attribute feel like a complete afterthought.

But okay, there is such a thing as an "optimal" delay, as in the minimum amount of time it takes for the player to travel from one gap to another. Well, this game is made to be played by humans, not bots. Players with many different variations of skill are going to have different definitions of what they find to be a "perfect" delay. Perfect is subjective.

Also, if you ever plan on making a level with rapidly fluctuation speeds, using delays that are calculated using the speed multiplier will lead to unpredictable results. That's why most pack developers don't even bother doing this.

That's why, in my engine, I have decided to fully expand on a concept that was only showcased in a Pseudogame example level, switching away from a temporal pattern system to a spatial pattern system. Instead of spacing your pattern with a unit of time, you are spacing your pattern with a unit of space (logical pixels). Instead of spawning walls at a fixed distance, the distance your walls spawn at is based on a dynamic reference "pointer" that passively moves like a "standard" wall. When constructing your pattern, you have to push back the pointer, and then spawn your wall at the newly positioned pointer. And of course, to ensure performance, the current spawn distance is now treated as a threshold value. When the distance pointer moves past the threshold, the engine permits a pattern to spawn. It will continue to spawn patterns until the distance pointer is behind the threshold.

How do you ensure sufficient pattern spacing for higher speeds? Good question. And I feel with this new spatial pattern system, it finally establishes purpose for the "delay multiplier" attribute. If you have a level that increments to a speed where the pattern becomes impossible to navigate, then you can separately increment delay multiplier accordingly (Fresh Eyes does this when speed multiplier reaches 3). So even if you have a level with a highly fluctuating speed, you can ensure consistency with pattern spacing. If you want more fine tuning with spacing of certain patterns, there are defaults in the pattern manager you can change.

No more overcomplications with "perfect" functions. Pattern construction and spacing is made much simpler and easier to control for the pack developer.

How do you fix the despawning issue? One issue with Baumm's implementation of a spatial pattern system is that lengthier patterns will not spawn properly as the distance pointer will exceed the despawn distance. This is because the despawn distance is a fixed value, like the spawn distance. In my pattern system, I solved this issue by "parenting" the despawn pointer to the distance pointer; the despawn pointer will always be X pixels behind the distance pointer. This distance can easily be configurable to your liking. Now you should only worry about despawning issues when pushing the distance pointer forward (which you shouldn't be doing often, and don't worry, my engine throws an error when this happens).

Fully 3D Visuals

Fresh Eyes

Open Hexagon, I think it's time for you to meet the third dimension. Granted, I am not the first to accomplish this. Epie accomplished a shader-based approach for 3D visuals. Baum wrote a quick and dirty implementation of 3D in a pseudogame example level. I say quick and dirty, because it only does the bare basics and does a couple of things wrong (which she says was done on purpose. Why?)

I built a lot on Baum's quick and dirty implementation to make it much more correct and modular. You now have a Canvas table to affect the game plane in 3D space, and a Camera table to affect the 3D "camera". Now you can do a lot more with the movement that isn't possible with regular Open Hexagon 2 or 1.92.

Timelines, Done Right!

In Open Hexagon 2, there are two default timelines: pattern and event timelines. From their names, they sound pretty self-explanatory: one is responsible for controlling the (previously) temporal patterns, and one is dedicated for events to occur throughout the levels. Of course, 1.92 had a finer control of events as events that were written in standalone JSON files each had their own timeline. This was reintroduced later in Open Hexagon 2 through "custom timelines".

However, when it came to the event and custom timelines respectively, the way you had to execute code on them was very hacky; you had to write your Lua code as a string. This method, once again, had some major issues that started to pop out as the engine aged:

  1. No error handling: When e_eval and ct_eval were written, they didn't factor in that there could be incorrect Lua code, both at the "load" time and the run time. When normally coming across errors in Lua code, the scripting engine will throw an error and kick you out of the level (if you're not in Debug mode). However, errors occuring in these code strings don't do this at all, and will crash the game! If the code you wrote for an eval function was more than a small snippet, this made debugging a giant pain in the ass.
  2. You can only use global variables and functions. Since these code strings have to be compiled at the top level by the engine, any local variables in your script that aren't in the string itself can't be used by the code string. The only way to get around this is by either forcing your local variables to be global, polluting the global table and impacting memory and performance; or you have to pass them into your snippet with tons of string concatenation, which also impacts performance considering that strings are immutable.
  3. No syntax highlighting. Because everything is in a string, if you are developing your packs in an editor which is meant to use a syntax highlighter, a linter, or a full language server, that all is useless and means nothing. It's all string! Good luck reading your code and catching errors ahead of time.

My custom engine reimplements timelines through the usage of Lua coroutines. And instead of passing direct snippets, you just have to write your code into a Lua function. This was an initial solution that was presented by Baumm, but of course I expanded on the concept to account for additional use cases and features.

  1. Proper error handling. On top of being able to catch lua errors ahead of time with a language server or linter, when my engine comes across an error in a timeline function, it will rightfully produce an error with the exact issue.
  2. Looping timelines. Need a sequence of events that can repeat over and over again? No problem. You can create looping timelines that will do exactly that.
  3. Change the speed of a timeline. Need to speed up or slow down a timeline? No problem. You can alter a timeline to progress at a different speed.
  4. More fine-grained control of timelines. You can proactively restart or even change the point in time of a timeline any time you would like.

Warning: Because of the buggy nature of coroutines on Open Hexagon 2, you will need a patch that will implement a workaround so they can work properly.

Introducing Better Rhythm with Music Timelines

Taking the custom timelines into consideration, I even Lua-ified the entire Music in a level and on top of that, built timelines that are specifically tied to music. In fact, Lua files that are dedicated to mapping events that are synced to a track are called beatmaps. There is no limit to how many timelines can be in a beatmap, so ideally, you can easily make a timeline that is dedicated to each song's stems or instruments, making it incredibly easy to enhance the experience of your level with precise and pristine timings to your song.

Fixed require, and a More Native Approach for Dependencies

In Open Hexagon 2, to import scripts and dependencies, you need to use u_execScript and u_execDependencyScript respectively. u_execScript is meant to do exactly what require does. So why are we reinventing the wheel? And on top of that, these functions don't even reap some of the unique benefits of require. I made a script that, with a bit of input from the pack developer, fixes require so you can easily start using it in your scripts.

By using require, your level scripts work better with linters and language servers that acknowledge require and its existence. My custom engine uses require all around instead of u_execScript. On top of that, a specific global function uses a more Lua-native approach to requiring dependencies, so importing dependencies will also work better with linters and language servers.

All Game Loop Functions Use a Custom Loader

Open Hexagon 2 has you incorporate all your level functionality through core Lua functions (e.g. onLoad, onStep). But there are some "required" functions where you don't implement anything. On top of that, onLoad is only supposed to run when you start the level, so why is it being run while you're in the menu? Can't you use onInput and onUpdate for the same thing? Yeah, there's some questionable decisions that were made over the years.

I made a custom loader that cleans all of this up. You can name the functions whatever you want, just as long as you bind them to the appropriate loader. And I also fixed some of the bugs in the behavior of these functions. Technically, you can have a level script that doesn't incorporate any of the functions (but then you will have a level that does nothing).

Music Used

Another primary goal that I had when making Demo Tapes was that I should completely forgo using copyrighted music altogether for the pack, and only use appropriately licensed music or getting permission from the artist. If you've never played Open Hexagon before, this sounds silly and like it should be common sense, but unfortunately, music usage is a huge problem in the Open Hexagon community.

While the main game's music is legal as it does receive permission from the artists, this isn't the case for nearly all community levels and packs. Pack developers in Open Hexagon have gotten away with using copyrighted music in their packs for over a decade at this point, and I wanted to use Demo Tapes as a way to showcase that you can absolutely have a fun pack to play without the need to infringe on copyright.

I've actually tried to bring up this issue multiple times with the community and how this can have a devastating impact on Open Hexagon's legacy. Unfortunately, most people handwave away the issue and pretend that it isn't a big deal. Open Hexagon is such a small game with such a small community, it's not like anyone is wanting to go after them, right?

Except We Have Precedent...

Unfortunately, there is one noticable case in Open Hexagon history where labels and music rights societies have spotted this issue and have taken action against someone for this. This person was named Alphatom Promethium45, but now goes by Abyssal (Abby). Abby was a noticable pack developer in the 2010s and made some really impressive packs for 1.92. Unfortunately, starting around 2018, Abby privated nearly all of her videos, including Open Hexagon videos, from her channel. The main reason: Copyright strikes. Some artists came across her video and weren't exactly happy with their packs essentially acting as a vessel for distributing music illegally. Abby was in a panic and didn't want her channel terminated.

It was only in mid-2025 where the situation improved and Abby felt comfortable to republicize some of her old videos. But this should absolute serve as a wake up call. Many other games, such as Geometry Dash, have figured out ways to appropriately use music for custom levels. Open Hexagon needs to find a way to design its engine to discourage (or even prohibit) the usage of infringing music. I wanted Demo Tapes to set an example in this.

Tracks

Another Bistro - Synthet

Why I Stopped Working On This

Now it's time to answer the million dollar question. Why is this pack unfinished? Well, in part, creating your own custom engine is no small feat. It takes a ton of time to make your own engine, and deep knowledge of the Lua programming language and game engine to do so. The first level, Fresh Eyes, was meant to establish all of the foundations of the engine so it was going to take much longer to make than the other levels that I was going to be making.

This isn't to say that subsequent levels weren't going to take quite a bit of time as well. If you thought that all of the features that are currently present in my custom engine was a lot, there were going to be more features. I was planning on directly modding and patching Pseudogame to introduce new features that I would need to use for my engine to sufficiently pull off certain concepts for levels. Plus, charting music and making beatmaps is an incredibly tedious procedure. I'm sure you can ask anybody who makes charts for a rhythm game and they'll tell you the same thing.

However, that alone wasn't going to stop me from working on this pack; I wanted all of that time and dedication to be spent making a perfect demonstration for what I envisioned the future of Open Hexagon to be. The real reason why I stopped working on Demo Tapes is for the same reason that Vittorio Romeo stopped providing regular updates to Open Hexagon: the community.

This isn't to say that everyone who is in the Open Hexagon community are bad people. There are certainly some amazing and well talented people in there. However, a lot of the people in the community are stubborn, immature, and pretentious. To put it more bluntly: They're assholes.

Considering the amount of time and effort I was putting into the engine, I wanted to journal my progress to the community by sharing what I have made so far. Nearly every time I did this, however, I often received criticism that didn't factor my plans, or just straight up unconstructive and insulting. There was nothing that I could say that would change their mind or have them understand why I was doing things the way I did. They were used to having things the way they wanted it to be.

Unfortunately, pairing this with other hostile interactions, the community couldn't even be bothered to show me basic respect. Since I founded and was basically head mod on a Discord server, a lot of the community even threatened to schism to another server just so they simply didn't have to be anywhere near me.

If that was only it, I could've put up with it and I would've continued with business as usual. Unfortunately, a lot of the other moderators on the team started doing the same thing. I even tried to be more vulnerable and honest with them, and most of them couldn't even return the favor by showing me basic empathy or acting like a human being. They weren't capable of saying "Sorry" whenever they did something wrong (the closest I got was a textbook example of a backhanded apology).

I had enough at this point and I decided to contact Vittorio myself and asked him to just demote me at this point. I had enough of this community's bullshit. I want nothing to do with this community anymore. This community can rot in hell for all I care. Thank you dipshits for ruining my passion for this game!

Will I Update This Pack?

Right now, absolutely not. I don't want to spend my time and effort giving to a community that will make lies about me or do other petty stuff to spite me.

This may change in the future, if I ever feel like it. If I ever do continue updating this pack, I certainly won't be listening to the community anymore.

About

Open Hexagon 2.1.7 pack that uses a custom engine on top of Pseudogame for next-generation concepts

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages