开源项目:一款16x9像素的节奏地牢爬行游戏
Show HN: An open-source rhythm dungeon crawler in 16 x 9 pixels

原始链接: https://github.com/jgalecki/qrawl-tiny-mass-disco

QRawl: Tiny Mass Disco是一款在Tiny Mass Games项目期间开发的16:9节奏地牢爬行游戏。游戏机制融合了基于节奏的输入和地牢探索,其灵感来自《Crypt of the Necrodancer》,并使用类似于“冰与火之舞”的系统来判断玩家输入时机,考虑“提前”、“良好”、“优秀”、“完美”、“延迟”、“失败”和“错过”等输入状态。“指挥者”类则标志着每个节拍的结束,从而促使游戏逻辑前进。 一个关键创新是“时间旅行”机制,用于在怪物行动之前解决玩家输入,确保与音乐同步。游戏状态会被保存,如果发生延迟但有效的玩家输入,它会在解决怪物行动之前应用于之前的游戏状态,从而确保公平的游戏体验。代码已在MIT许可证下开源,鼓励其他人探索和构建基于节奏的地牢爬行机制。游戏诙谐地利用了16:9的限制,在游戏结尾显示一个二维码,暗示未来将会有一个项目可以从现实世界的二维码生成地牢。

这个Hacker News帖子讨论了“Show HN: 一款16 x 9像素的开源节奏地牢爬行者游戏”,这是一款使用Godot引擎开发的游戏,灵感来自《Crypt of the Necrodancer》。开发者jgalecki开源了代码(MIT许可证),并以CC0许可证发布了像素精灵,其创作动机源于一次游戏开发挑战。 评论中重点提到了节奏游戏开发的挑战,尤其是在NES限制或低分辨率等约束条件下。用户们讨论了时间旅行机制、节拍调整和动画技巧。一位评论者指出美泰的电子足球游戏(3x9像素)是低像素游戏的一个先例。 Jgalecki提到在2023年的Roguelike Celebration上做了一个关于程序化谜题生成的演讲,并提到一个《Necrodancer》多人游戏项目对其有所启发。讨论还涉及到Tiny Mass Games项目,其开发者使用了各种引擎,例如Godot、Unity、Unreal、Construct 3,甚至还有Hypercard。
相关文章

原文

release gif

Hello! Welcome to QRawl: Tiny Mass Disco, a 16x9 rhythm dungeon crawler. This game was created as part of the the Tiny Mass Games project, a loose collective of game devs focused on creating polished short-form games in two-month dev cycles. This game came out of our tenth season, and I decided to open-source the code after completing it. The official web version of the game can be found at https://joshuagalecki.itch.io/qrawl-tiny-mass-disco.

There are two important things to understand in terms of designing a rhythm dungeon crawler. The first is the rhythm part of it. For the basics of the Conductor class and creating a rhythm game, I'll refer you to fizzd's legendary tutorial on A Dance of Ice and Fire, archived at https://web.archive.org/web/20190317082117/http://ludumdare.com/compo/2014/09/09/an-ld48-rhythm-game-part-2/. Honestly, I've done three rhythm games and prototypes now and I dredge that up every time. Classic.

The second important thing to understand is the dungeon cralwer part of the game. Many rhythm games don't have to deal with this side, as something like Guitar Hero only rates input instead of really reacting to it. Lots of cues have been taken from Crypt of the Necrodancer, since they invented the genre combination.

The player's input can be in eight different states:

  • None: The player has not put in any input yet, but they still could.
  • Early: The player hit a direction, but it was too long before the beat to count as good input. This is a failure input.
  • Okay: The player hit a direction, but it was a decent bit before or after the beat hits. Barely good enough to count as a good input.
  • Good: The player hit a direction and it was acceptably within a window of time around the beat.
  • Great: The player hit a direction and it was very close to the beat.
  • Late: The plaer hit a direction, but it was too long after the beat to count as a good input. This is a failure input.
  • Failed: The player hit a direction, but the requested direction was illegal. (For instance, trying to go into a wall when you can't dig.) This is a failure input.
  • Missed: A beat closed out without any input from the player. This is a failure input.

Here's a chart showing the timings. It may seem obvious, but it's worth mentioning that a player can hit valid input on either side of the beat.

Input Rating Explanation

When we are 25% of the way to the next beat, the Conductor sends out a beat close out signal. This lets us wrap up any logic related to that particular beat. For instance, if the player hasn't moved by that point, they aren't given a chance to. Technically, they missed their chance after the OK envelope closed. The point of the close out signal is to signify that the beat is closed and the game logic should move on. Mistimed input immediately after the close out signal now counts as an Early input for the next beat instead of a Late input for the previous one.

(A small aside: why not have a close out signal right after the OK envelope expires? That gives us a chance to mark the input as Late for the old beat. You can have a Great / Good / OK input for 0.06 / 0.12 / 0.18 beats after the beat hit, and then Late input until we're at 0.25 beats. It wouldn't be very sporting for a player to have an input at 0.2 beats, miss the OK on one beat and get marked off as Early for the next beat.)

So, now to my favorite part of the code - time travel! As fizzd says, we want everything in the game to respond to the beat, right? But, we also want the monsters to wait until after the player has provided input. If a player is being chased by a ghost and the ghost is going to attack on the beat, how do you accept a late-but-valid input from the player? (Let's say they hit an input 0.05 beats after the beat hit, well within our envelope.) Making the monsters wait until after player input (or after the beat closes out) looks awful, as they won't be sync'ed up to the music at all.

Time travel is the solution, of course. I keep the entire game state in a single object. In one specific case - when the beat hits but player input is still None - I keep a copy of the old game state (previous_game_state) and update the current state (game_state). If the player doesn't do anything and ends up with a Missed input, then the current game state just keeps being the current game state. If the player does hit a late-but-valid input, then we pass in previous_game_state to the player_controller's update function. If we leave the update function with a valid move from the player, then we'll set previous_game_state to be the current game state and then run all of the "beat hit" updates (that were run on the "old" current game state when the beat hit).

Time travel is always confusing, so gaze upon this graphic.

Time travel explanation

  1. This is the previous beat, call it beat 40. The slime is about to move forward into the player's square. If the player is still there, the slime will stay where it is and attack the player. The current game state is in yellow.
  2. Beat 41 hits. No input has been received by the player. Save the current game state as previous_game_state (in pink). Resolve the current game state (in yellow), where the slime attacks the player and the player loses health.
  3. Just a bit after beat 41, the player puts in an input. This resolves against previous_game_state (in pink). It's a valid input, so previous_game_state (with the player's updates) becomes the current state. The slime moves forward without attacking the player, since the player is no longer on the same square.

That's the tricky part about dungeon crawling to the beat! Hopefully you've found this guide helpful, either for playing around with this codebase or perhaps implementing your own rhythm dungeon crawler logic.

As a final aside, I cheat the game's 16 x 9 pixel restriction twice. Once for credits for legibility / time's sake, and once after dungeon is beaten. Winning the game reveals that the whole dungeon is a giant QR code. I have a future game idea to make another QR dungeon crawler (the "Qrawl" of the game's title) that can take a picture of any random QR code and make it into a dungeon. I love the idea of taking ads and other intrusions into our daily lives and turning them into our own playgrounds.

This project is licensed under the terms of the MIT license

联系我们 contact @ memedata.com