Pocket Dimensional Clash 2

Complete Pocket Dimensional Clash 2 2.3

No permission to download
Project is completed.
Note: Oops, it looks like we forgot to turn the enemy info (icon, name, and health bar) back on when we recorded this video.
We’ve fixed it now—that info will always be visible; personally, I’ve never liked beat-'em-ups that don't show it.
 
Dude, I'm always dying for having an animated subscreen for huds, but I struggle.

Looking great, man! I really like the animated subscreens with animated hearts playing as a lifebar as well as the animated MP bar.
 
Dude, I'm always dying for having an animated subscreen for huds, but I struggle.

Looking great, man! I really like the animated subscreens with animated hearts playing as a lifebar as well as the animated MP bar.
It was @DCurrent 's wizardry. I look at the code and understand almost nothing, haha—and that’s even with it being fully documented and organized.

He can explain it better, but as far as I know, each heart is a subscreen too, just like the MP bar.

We use additional subscreens on the selection screen as well—one for the portraits and another for the character and game names.
pdc - 0085.png

And the select screen works in-game too:
pdc - 0086.png
 
  • Like
Reactions: NED
He can explain it better, but as far as I know, each heart is a subscreen too, just like the MP bar.

The individual elements - hearts, meters, icons, text, etc. are not sub-screens, but HUDs, the scrolling background, and the "pipes" are.

Before explaining the layers, there are six sub-screen basics worth knowing:
  1. Items are positioned relative to the sub-screen. The hearts, for example, are arranged within their player’s HUD panel. Moving the HUD means moving its screen. Everything inside moves together and keeps its layout.
  2. Width must be a multiple of four. The engine rounds the requested width down when necessary. Height is used as supplied.
  3. Drawing order determines priority inside the screen. Later draws cover earlier ones. The completed screen receives its own Z position in the main sprite queue.
  4. When background transparency is enabled, RGB (0, 0, 0) pixels are transparent. Disabling it makes those pixels draw as black.
  5. Pixels stay until you overwrite or clear them. Unchanged screen contents can be reused across frames.
  6. Each screen submission occupies one entry in the engine’s 5,000-item sprite queue, regardless of how many elements were drawn into it.
The first four guide construction and design. The last two provide opportunities to optimize.

For handling logic, the HUD system leverages OpenBOR’s ability to compile scripts on the fly, giving each HUD its own self-contained script instance. The script builds its display within a sub-screen, while the manager handles its placement. Contrary to popular understanding, OpenBOR very much supports object-oriented programming and polymorphism - we’re using both here to build independent HUD objects with their own state and interchangeable implementations.

Each player slot owns its renderer instance and private meter data. Characters use the default renderer unless their model specifies a custom script. Multiple players can use the same script source while maintaining independent display state. This lets us customize one character’s HUD or reposition a complete panel without rewriting the surrounding system.

Once everythign is ready, it gets composited onto the sub-screens. From back to front, the current arrangement has four layers.

1. Scrolling backdrop

The backdrop uses a 480×56 sub-screen at X 0, Y 216. The scrolling texture is tiled into it first, followed by a stationary mask that writes transparent black over the unwanted areas. The completed screen sits behind the player HUDs.

2. Individual player HUDs

Each player has a 108×44 sub-screen at Y 228, with X positions of 11, 128, 246, and 362.

Each renderer reads its current actor’s information and draws the applicable elements. Meter sheets define the layout, value sources, and display conditions; a support library called dc_filmation supplies the animation artwork. This produces the hearts, MP gauges, portraits, names, scores, and other contents of each panel. Both the HUD and dc_filmation leverage the engine's ability to read files. So instead of hard coding animations and layout, they both use sheets that I designed to be very similar to the engine's native text files:

Here's a snippiet from the animation sheet - the belt scroll for HUD background:

Code:
# Shared full-width HUD layers. The pattern repeats every 32 pixels.
# Filmation applies belt_notch after returning to the minimum, so -1
# produces a 0..31 loop.

animation player_hud_back_scroll

    belt_delay 32
    belt_loop x_return y_return
    belt_notch 1 1
    belt_position -1 -1 31 31
    dm_config active reset_pre reset_post
    dm_enabled 1
    frame data/sprites/hud/player_hud_back_a_0.png

3. Pipe bezel

The bezel uses another 480×56 sub-screen at X 0, Y 216. Its shared pipe artwork frames all four player panels. Keeping it separate lets the frame stay stationary while the backdrop scrolls and the player displays change. The renderer also supports animated framing artwork.

4. Native HUD text

We let the engine's native HUD supply the credits, “Game Over,” and “Press Start” text. Everything else is moved offscree to disable it. It would be pretty easy to handle this part with custom elements too if we wanted.

HTH,
DC
 
Here is a rundown on the selection system...

The selector uses a flattened indexed array of 72 slots, representing 18 columns * 4 rows. Each occupied slot stores a pointer to the model’s database record.

The model database reads the model sheets, and selection logic uses that information to determine eligibility and placement. Models can specify their own grid coordinates, and those positions get priority. The remaining eligible models fill available spaces from left to right, row by row, within columns 1 through 16.

Reading and parsing all those files is slow - really slow in computer terms. Preparing all the data takes about 0.25 to 0.5 seconds on a low-spec machine. That's a lifetime in live play, but a microcosim in loading, which is where we do it. To make it even less noticable, we spread that work across the loading routine, adding records as models become available and skipping those already processed.

Then, once the data and selection array are populated, it’s brutally fast. Player browsing, random selection, and other selection operations work from optimized records already in memory. Combined with reusing the finished sub-screen between changes, that lets the selector run during live gameplay with no worries at all.

Drawing the grid​

The visible grid is a single sub-screen containing the striped background, grid lines, model icons, player cursors, and control instructions. Each icon’s position comes from its cell in the array, with coordinates relative to the sub-screen.

Highlighted icons appear in their original colors, while the others receive a darkening tint. The outer columns, 0 and 17, receive background fill and borders only when a player highlights a cell there.

Reusing the finished picture​

The grid’s contents stay static between selection changes, so we only rebuild the picture when its display state changes, such as when a player highlights another model. Plus, as noted above, a sub-screen is one item in the sprite que. So even though we've blitted several dozen elements to the sub-screen, the final draw is just one sprite.

Masking unused cells​

During live gameplay, the renderer draws RGB (0, 0, 0) boxes over empty cells after drawing the background and grid lines. Remember the sub-screen transparency rule: those zero-valued pixels become transparent when the completed screen is displayed, revealing the gameplay underneath.

The renderer restores the borders of occupied cells, then draws the icons, instructions, and highlights. This keeps the selection overlay’s visible footprint limited to the space its contents need.

ss_diagram_0_b.png

HTH,
DC
 
Looking at the character select screen, I got the impression that you should be able to navigate up, down, left, and right, much like in MUGEN or other fighting games. Is it actually limited to just left and right selection?
 
A new character has been added to Pocket Dimensional Clash 3—and this time, it’s an original one: Crafty Coyote, bringing Darryl Flood's character to life.

We’ve always wanted to bring a boxer into the game, and now we have one :)
Full video:
 
Jhfer—my longtime partner who, besides being a great friend, is a walking encyclopedia of comic books—is constantly working to bring new characters to *Pocket Dimensional Clash*.

I know some people might find it odd that he’s creating content for *PDC4* while we’re still programming *PDC3*, but he always works ahead of the curve, and we like to plan things out; we almost never add a character just for the sake of it—unless we really love them.

This time, he’s bringing not just one, but two new characters. Can you guess who they are?
new_char.png
Hint: They’re from comic books, and from rival companies.
 
I decided to test Claude (*my thoughts on this below) to assist with some tasks in the development of my projects, and the results were very good.

For a long time, I’ve wanted to implement a boomerang mechanic for a character—one that would need to follow a specific set of rules and account for various scenarios. In OpenBOR, we work with four axes (X, Y, Z, and B—the latter representing the character's base position relative to the ground or a platform), and there could be walls in the way as the boomerang returns to the character who threw it, among other complexities.

OpenBOR uses two languages: a native language and a C-based interpreter, each with its own quirks. I had previously tested Claude using pseudocode, which worked, but this time I decided to try it with actual code. To my surprise, it generated both the C code and the native code, with a high success rate in both cases (though even better with the C code).

After some back-and-forth, the code works exactly as I intended. Claude proved essential for a key aspect: debugging. It didn’t just "write the code"; it engaged with me, outlining potential scenarios, while I—drawing on my own experience—guided the process and made the final decisions. An important caveat: my prior knowledge was crucial for steering the development and preventing errors. For instance, Claude might suggest C features not supported by the OpenBOR interpreter (like `abs`), or occasionally propose a solution far more complex than what was actually necessary.

It was a very positive experience—especially for me, as I have very little free time (which impacts the development timeline of my free games, since I have to prioritize projects that provide a financial return).

*My take on using AI in programming: I’m not against "vibecoding" per se. But I believe that using AI is like using a chainsaw—if you know what you’re doing, it’s a great tool; if you don’t, it’s a disaster waiting to happen.
When used to help a developer—someone who already has programming knowledge—save time, I think it’s perfectly valid. What I am completely against is when someone with no knowledge in the field tries to just press a button to have it "do the work for them."


 
I decided to test Claude (*my thoughts on this below) to assist with some tasks in the development of my projects, and the results were very good.

For a long time, I’ve wanted to implement a boomerang mechanic for a character—one that would need to follow a specific set of rules and account for various scenarios. In OpenBOR, we work with four axes (X, Y, Z, and B—the latter representing the character's base position relative to the ground or a platform), and there could be walls in the way as the boomerang returns to the character who threw it, among other complexities.

OpenBOR uses two languages: a native language and a C-based interpreter, each with its own quirks. I had previously tested Claude using pseudocode, which worked, but this time I decided to try it with actual code. To my surprise, it generated both the C code and the native code, with a high success rate in both cases (though even better with the C code).

After some back-and-forth, the code works exactly as I intended. Claude proved essential for a key aspect: debugging. It didn’t just "write the code"; it engaged with me, outlining potential scenarios, while I—drawing on my own experience—guided the process and made the final decisions. An important caveat: my prior knowledge was crucial for steering the development and preventing errors. For instance, Claude might suggest C features not supported by the OpenBOR interpreter (like `abs`), or occasionally propose a solution far more complex than what was actually necessary.

It was a very positive experience—especially for me, as I have very little free time (which impacts the development timeline of my free games, since I have to prioritize projects that provide a financial return).

*My take on using AI in programming: I’m not against "vibecoding" per se. But I believe that using AI is like using a chainsaw—if you know what you’re doing, it’s a great tool; if you don’t, it’s a disaster waiting to happen.
When used to help a developer—someone who already has programming knowledge—save time, I think it’s perfectly valid. What I am completely against is when someone with no knowledge in the field tries to just press a button to have it "do the work for them."



Nice @O Ilusionista!

I have no problem with generative tools, assuming they're used as tools and not crutches. I do the same myself. Most of my recent development work is heavily boilerplated using ChatGPT's professional tools. Those require a paid subscription and some investment in establishing project context and conventions, but the results are very good.

Of course, the tools have a tendency to overcomplicate things, and I still have to rework quite a bit of the output. Even so, the process is considerably faster than writing everything from scratch, often with fewer tedious implementation errors.

The biggest advantage is that it frees you up to concentrate on the engineering aspect - which, as any experienced developer knows, is where the really important work happens. Writing code and engineering software are two very different things. There's a reason computer science is so closely associated with engineering in the first place. The fundamental mistake people make with "vibe coding" is allowing the generative tool to handle the engineering decisions, too. That's where things get dangerous. You're effectively handing over the design of a system to something that doesn't actually understand the requirements, constraints, or consequences of its own decisions.

To be very clear, design means outlying the fundamentals that make a system work, not "my cool ideas."

For example, you should be asking something like:

"Implement an arctangent-based curve to gradually reduce X velocity, reach zero at a specified distance, then reverse direction. Here are the available primitives and constraints..."

Or, if you don't yet know enough to be that specific, ask something like:

"How can I reverse an object's direction after it travels a certain distance, like a boomerang? What are the basic principles involved?"

What people do instead is:

"Make me a boomerang."

Two of these are not like the third.

The first presents a specific engineering problem, establishes constraints, and delegates the implementation. The second uses the tool as a teacher, helping you acquire the knowledge needed to understand and eventually solve the problem yourself.

The third simply delegates the entire problem-solving process, including decisions the person making the request might not even know need to be made.

That's the real distinction. You don't necessarily need to understand how to solve the problem before asking AI for help. You do need to take responsibility for understanding the solution before putting it to work.

DC
 
Of course, the tools have a tendency to overcomplicate things, and I still have to rework quite a bit of the output. Even so, the process is considerably faster than writing everything from scratch, often with fewer tedious implementation errors.
Yes, For example, looks like it doesn't understand the concept of "landframe", so it scripted one

In just two days, I've solved some bugs which were hauting me for months (one was on my game for more than a year).
I had a ondreathscript which goes through all the available entities to turn a specific entity into a boss. But I noted this only works if there are few entities on the screen - if you are playing in co-op, it never triggers.

I even thought the `ondeathscript` event wasn't triggering fast enough, but it turns out the cause was something else (plus a mistake on my part):
Probable cause

`openborvariant("count_entities")` returns the number of existing entities, but `getentity(i)` indexes the engine's list of slots, which may contain "gaps." When there are few entities, they occupy the initial slots, and a loop running up to `count_entities` will reach them. However, as more players and enemies appear, entities are removed and slots are reused; consequently, the boss might end up at an index higher than `count_entities`. The loop terminates before reaching it—which explains why the issue only occurs when there are many entities.

The correct limit for the loop is `openborvariant("ent_max")`, which represents the total number of slots. I haven't been able to test this, so please verify using the log below.

And once I changed (and removed the unnecessary trigger), it works. It's a great tool if you know what you are doing.
 
Nice boomerang action! It reminds me to boomerang I've coded for Double Dragon Mini years ago. It doesn't work like this but it was meant for example.
Thanks! It uses a fairly complex mechanic that I wasn't able to implement on my own:

- When fired, it travels in a straight line while gradually decelerating.
- Once its X-axis velocity drops below 0, it begins to track its "parent" object across the X, Y, and Z axes.
- Upon nearing the parent, it checks several conditions: if the parent is falling, dead, or no longer exists, the boomerang immediately drops to the ground. If the parent is in one of four specific states (idle, walk, block, or faint), it forces the parent into an animation that simulates catching the boomerang. Otherwise, it continues to track the parent until contact can be made.
- If it hits a wall while moving forward, it automatically ricochets at the same speed, without any deceleration (I still need to tweak this, though—if it’s fired while already inside a wall, it gets stuck).
- If it detects a wall while returning, it doubles its Z-axis velocity to maneuver around it, moving either "up" or "down" depending on the parent's position.

Initially, I ran into an issue where the boomerang would immediately appear in its "on the ground" animation state. Claude suspected it was due to a missing "parent" property within the `spawn01()` function, but my version of that function—which includes a safety check—does actually assign the parent.


C-like:
void spawn01(void vName, float fX, float fY, float fZ)
{
    //spawn01 (Generic spawner)
    //Damon Vaughn Caskey
    //07/06/2007
    //
    //Spawns entity next to caller.
    //
    //vName: Model name of entity to be spawned in.
    //fX: X location adjustment.
    //fZ: Y location adjustment.
      //fY: Z location adjustment.
    void self = getlocalvar("self"); //Get calling entity.
    void vSpawn; //Spawn object.
    int  iDirection = getentityproperty(self, "direction");

    clearspawnentry(); //Clear current spawn entry.
      setspawnentry("name", vName); //Acquire spawn entity by name.

    if (iDirection == 0){ //Is entity facing left?                 
          fX = -fX; //Reverse X direction to match facing.
    }

      fX = fX + getentityproperty(self, "x"); //Get X location and add adjustment.
      fY = fY + getentityproperty(self, "a"); //Get Y location and add adjustment.
      fZ = fZ + getentityproperty(self, "z"); //Get Z location and add adjustment.
    
    vSpawn = spawn(); //Spawn in entity.

  if (vSpawn){// safe check

    changeentityproperty(vSpawn, "position", fX, fZ, fY); //Set spawn location.
    changeentityproperty(vSpawn, "direction", iDirection); //Set direction.
    changeentityproperty(vSpawn, "parent", self); //Set parent.
    
    return vSpawn; //Return spawn.
    
  }
}

As it turned out, because the boomerang has a delay of 1 and the script runs inline, the execution was so fast that by the time the function assigned the parent to the spawned object, it was already too late—we confirmed this using `log()`.

---

Another issue I managed to resolve—though it took quite a while—is that the *Golden Axe* bonus stage now features a victory screen with a scoring system. It works as follows:

- I already had an individual counter that tracked how many barrels each player broke (courtesy of Kratus).
- This counter monitors the number of enemies on screen (barrels are converted to the "ENEMY" type upon spawning). As soon as it detects the first enemy, it changes its own type from "NONE" to "ENEMY" to prevent the level from ending prematurely.
- The code was struggling to keep an accurate count of the remaining barrels—specifically regarding why some barrels granted points upon destruction while others didn't. That was my fault; I hadn't provided enough context to Claude, failing to explain that barrels roll across the screen and are removed if they go beyond the screen boundaries, without triggering the counter.
- Once all the barrels are gone, a function is triggered that locks all player inputs, tallies the results, and applies the following rules:
If one player has destroyed more barrels than the others, they are declared the winner—triggering a victory animation for them, while the other players perform a defeat animation.
In the event of a tie (whether all counters are at zero or two or more players have the same count), it’s declared a "draw game," and all players perform the defeat animation.
If there is a clear winner, the total number of barrels they destroyed is multiplied by 1,000, and this amount is awarded to the winner as a bonus.

Finally, a message is displayed identifying the winner, the number of barrels they broke, and the bonus amount. It also assigns a rank based on the number of barrels destroyed—with the code taking the currently active language into account.
pdc - 0098.png
 
Back
Top Bottom