First Person "3D" Maze & Relative Movement

That’s a-maze-ing! :grinning:

4 Likes

I've rewritten this to handle any arrangement of walls! Now you can explore whatever open spaces you like. Here's a demo walking around the default title card letters in first person!

first_person_maze_demo_3

As always, here is the json:

First Person Maze.zip (7.0 KB)

The perspective gets a little squiffy, especially for walls visible 2 tiles away, but I think the brain makes sense of it. Or maybe just mine does because I've been staring at it so long!

It also has that classic effect from a flat draw distance that means you can see further in the diagonal directions. I've played enough videogames that do the same to not worry myself over that :smile:

Possible further ideas floating around my head: Add a compass, add visible character sprites/items, draw different walls depending on the tile name, (e.g. doors), turn the default top-down view into a "map" accessible from a menu (where maybe walls only become visible on the map if you have been near them), try some random room generation... or maybe just optimise the current code :sweat_smile:

10 Likes

Dang. That is just ridiculously cool. Would you care to briefly explain how this works? Is it doing something like raycasting? Perhaps I should just read the source code.

Would walls of different textures be possible? How about floors?

Nothing so fancy I'm afraid! I wouldn't know where to start.

It's mainly brute force. After blanking the screen and hiding the player, I start by checking every tile in potential view of the player to determine whether its solid or not. That's every tile 4 tiles ahead from the player's perspective, and at furthest 4 tiles to the left and right. With all of those values I then just start drawing every tile, starting with the furthest row and working from the outside in, that way any solid tile closer to the player will get drawn over whatever was behind. That means I'm almost always wasting time drawing things that will never be seen and drawing over the same coordinate multiple times, so it's not very smart or efficient!

The implementation in my first two attempts is a bit smarter, as it does conditionally only determine if a tile is solid if it needs to be drawn, and will not waste time drawing over itself. This proved difficult to expand and I quickly got into a mess of nested conditionals, which is why I took the brute force approach instead.

I don't know how performant this will be on an actual playdate, I suppose it depends how costly the draw function is.

Both should be simple additions I think! With the floor, simply drawing this in before drawing the walls will mean they get drawn over the floor and all should be good.

With the walls, these are tiles drawn like draw "top right corner" at 7,6. At the same time as checking the solidity of each tile you could also check each tile's name, and then change all the draws to be like draw "top right corner {tileName}" at 7,6 - then each wall will use the correct tiles (as long as you've made them!)

3 Likes

Thank you for the very clear explanation. If performance is an issue, you might be able to do a very simple culling based on only the closest three tiles.

Did you write the pulp script directly, or did you write a meta-program that generated it?

Also, what kind of game are you planning to make?

I wanted to better understand your code, so i started playing with it. In the end I refactored your code to break the logic into more modular/reusable chunks. I leveraged the "mimic" feature to put this logic into separate tiles for this:
First Person Test.zip (9.2 KB)

With this change you can now tap the B button (aka cancel) to toggle movement modes. As you originally created this with 'relative' motion (i called it 'vector' mode in comments), which is really required in the 3D view... but in the 2D view the standard pulp movement mode (i called it 'XY' mode in my comments) potentially makes more sense to players. So, now you can tap the B button to toggle that movement mode. I added some logic to force players to switch to use vector mode in the 3D view though.

I really like this - it feels like a very reasonable approach to pseudo 3D in Pulp Script.

Next up for me, assuming you don't mind me stealing your code and running with it, is to integrate this 3D view into my 'e-motive' game. For that I plan to stylize the floor and wall tiles to represent artifacts, hazards, and wall types (rocks, cliffs, etc). I think this could be done by creating additional frames for the perspective view wall tiles, one frame for each style i'd like to draw, then switching the tile frames when drawing them.

[EDIT: playing with this, i think it might be nice to have a 3rd mode where left/right strafe rather than turn or turn+move. With that mode a user in the 3D view could strafe left/right and get a better sense of their surroundings. maybe switching movement modes will be better in a menu then, with illegal mode(s) somehow greyed out? what do you think?]

[EDIT2: turns out that adding strafe mode is really easy, so i just added it:
First Person Test.zip (9.4 KB)
I no longer stop the user from switching to an "illegal" mode... so tapping the B button will open an ask prompt making you pick from any of three modes without regard to your current view]

[EDIT3: ok, not as easy as i thought. still needs an update to account to ensure that left/right strafing makes sense when you aren't facing north. basically it needs something to convert left/right into a relative movement which might be up/down, down/up, left/right, or right/left depending on which way you are facing. not a big effort, but i need to get back to me day job here so i'm leaving it undone for now]

[EDIT4: dang it, don't tell my boss. i took a few minutes and updated strafe to work properly:
First Person Test.zip (9.8 KB)
The variables I calculate for strafe movement should probably be precalc'd just as the original code did, and string comparisons are probably a stupid thing to use... but the code functions. i'm done now... well, if i make another change i'll at least post it in a new comment rather than an edit here!]

-bit

3 Likes

Nice - I didn't realise you could use mimic in the player script like this to organise code! Fun to see where you were picking apart the code to better understand it :slight_smile:

Of course, go for it! Thanks for asking and I look forward to seeing what you do with the idea.

I had been thinking about this myself! I was thinking along the lines of using the d-pad to strafe, and rotating the crank to turn left and right, in a playdate approximation of standard dual analogue first person controls. If you automatically switch between that one control scheme in first person and absolute ("XY") movement in top down 2D then no menu is required. Not as fun to code and test that out in the browser player though, I want the actual hardware!

I was going to add that strafe wasn't quite working right as it was assuming the player to be north facing... but I see you have been editing as I type for the same reason :smile:

1 Like

oh i like it!

Updated to incorporate exactly that, using absolute angle of the crank and a trigger window of +/- 30 degrees around absolute N,S,E,W directions. So this version uses XY movements in 2D view, then STRAFE movements in 3D view with the crank allowing directional controls.

Also added logic to automatically parse the crank when switching to 3D view. this is a little confusing to the player... if you are in 2D view and facing north, but the (currently unused) crank is pointing south... then you switch to 3D view... i added logic to automatically parse the crank and therefore rotate you to face south. if you don't touch any other controls and just switch immediately back to 2D view you'll see the player is now facing south.

does that make sense? i see two alternatives:

  1. don't rotate the player until/unless they turn the crank (just comment out a single line of code for this)
  2. use relative angle instead of absolute for rotating the player in 3D view (i started with this but didn't like it as much)

First Person Test.zip (10.1 KB)

2 Likes

Awesome! It's fun even in the browser :smile:

I wouldn't worry too much about the transition between 2D and 3D, it's a contrived situation anyway right?

1 Like

You could make the 2d mode me a map. When you bring it up, you can't move.

2 Likes

:partying_face:

maybe? i haven't figured out exactly how e-motive will play out, but i suspect shifting between 2D and 3D will make sense in the game... in my concept the player will create small programs that run on a robot and they will sometimes sit back and watch the robot do its thing... during those times i can definitely see players shifting between views.

the full SDK just released! i'm excited for this - lua looks like a bunch of nonsense to me, but C is nice and comfy so i'm eager to dig in! :slight_smile:

After playing around with implementing some maze generation algorithms and a few other ideas, I threw them all away and came up with a different concept entirely! Introducing...

Daedalus Versus Minotaur

Based on the ancient Greek myth of the Labyrinth, dvm is an asymmetrical versus game of maze building and solving.

dvm_screenshot_01

dvm_screenshot_02

dvm_screenshot_03

dvm_screenshot_04

dvm_screenshot_05

Two competing players take turns, one building a maze in top-down 2D for the other to solve in first person pseudo 3D - each with a frantic 30 second time limit. Every other round the players alternate roles between builder and solver as the overall score is tracked. A successful escape is a point for the solver, an unsuccessful one a point for the builder. The exit the solver is aiming for is the last position the builder was at when their time ran out.

Playing against myself makes the solving somewhat easier than intended, but I have playtested it with my partner just now (looking away while the other builds but spectating the escape attempt) and I was pleased to be having fun! It's not a concept I've seen before and I think it works pretty well :slight_smile:

Some maybe-interesting development notes:

I implemented the "carve-able" labyrinth as a room full of sprites with the appearance of black tiles. On interact, which is called on a sprite when the player attempts to walk into it, it swaps itself to a white tile and calls goto on the player to move them into its position. This results in the building gameplay code being incredibly simple!

The downside was that my existing relative movement code didn't yet support sprites (as the player's bump event is not called when bumping into a sprite, only a solid world tile), so I had to fix that. There's probably a better way of doing it if you have multiple sprites to worry about, but I just have the one so just added the necessary logic to the sprite's on interact handler.

I did as I suggested earlier in the thread and expanded the first person view vars to get each tile name as well as solidity so that I can display the exit in first person. No fancy graphics for this, I just have a "!" drawn as a label in the relevant position. It's quick and dirty but workable!

Here is a zip of the json so you can play it and see the code:

Daedalus Versus Minotaur.zip (8.4 KB)

If anyone gives it a play, please do let me know what you think (especially if you try it in actual multiplayer!). I'm not sure what the balance is like between building and solving but I'm pretty sure it favours builder wins.

One broken strategy that I haven't addressed is:

Both builder and solver have the same input rate/delay, which gives an advantage to the builder due to their absolute movement. If the builder just makes a zig-zagging corridor, they can build that quicker than the solver can navigate it due to the builder moving at a rate of 1 tile per button press but the solver, who has to rotate the camera before moving around a corner, moves at a rate of 0.5 tiles per button press. With the same time limit that means the builder can just zig-zag and race away, although there is still the player's input time and relatively small screen space that might mitigate this a little.

3 Likes

This is really neat! Didn't try it with a friend yet, but would like to!

I wonder if you'll want to give the builder slightly less time to try to balance it a little better.
Maybe also consider letting the solver take a lot more time, but incorporate how quickly they solve it into the scoring.

I also think you really need a replay after the solving attempt – show the overhead view and reply the steps the solver took :smiley: It'd be fun for the solver to get to see the maze from overhead and see where they wandered around.

Nice work!

1 Like

All great ideas! I'm thinking of trying just 20 seconds to build and 40 seconds to solve. 20s isn't long and might be the bottom end of time to allow enough interesting complexity, but then 40s is quite long and I certainly don't want to push towards a minute. The solver having twice the time of the builder aligns with my balancing thoughts around tiles moved per key press.

On scoring I'm considering a bonus point if the maze is solved in less than half the time available, but I like the symmetry and simplicity of 1 point being up for grabs to both players per round.

Regarding a replay I had thought the same but then also thought it would be too hard to implement without arrays. I considered each tile could be swapped as it is moved over and that could be used to record the solver's path, but this quickly gets complex when you realise they can walk back over themselves. However thinking about it some more I think just a still image showing the path taken, rather than a replay, might be just as good, and I can do that relatively easily by swapping out white tiles with tiles showing a path take in and out of the tile. At most complex a tile that was revisited might show a crossroads, and that wouldn't preserve information about which route was travelled first, but the overall picture of the maze and what parts were visited would all be there!

I'm going to give those things a go :slight_smile:

1 Like

Great point about the difficulty of replay.
I like the idea of just showing the path taken. That may be even more satisfying.

1 Like

New version!

Daedalus Versus Minotaur.zip (9.4 KB)

Time limits have been rebalanced to 20s to build, 40s to solve.

The bigger change is that you now get a summary at the end of each round showing the maze and the path the solver took:

dvm_screenshot_06

Thanks @jestelle for the feedback, this is definitely way better!

To achieve this I made passable world tiles for every combination of the path going through the cardinal directions into a tile. The game keeps track of the direction you enter a tile from, the direction you leave, and if a path is already drawn on that tile, and then combines all of those to know which path tile to swap in when you move on.

2 Likes

Awesome!

I really can't wait to get the Playdate hardware to be able to really play test these games :smiley: I suppose I can call my kids down to my office to play this with me - but I can't wait to run upstairs with the Playdate in hand and say, "hey kids, play this new maze game with me" :smiley:

1 Like

This is a bit of a bump - but I just released a full game on Itch: Daedalus Versus Minotaur by Orange Thief

And here's a gameplay vid: https://www.youtube.com/watch?v=6ftWvYCh8vo

There are a lot of changes compared to what I shared above, most notably the approach to drawing the first-person perspective got almost entirely re-thought and re-written. When I got my playdate and tested out the original project it ran terribly when in first-person, hitting maybe 3 fps. It was almost entirely unplayable! I realised I needed to drastically change my approach, but was a performant full screen first-person maze view even in the realms of possibility? What I needed to do was think up all the possible ways it might be possible in Pulp and do some performance testing.

Possible approaches for effective full screen draws:

  1. Brute force draws - just draw 375 tiles to cover the entire screen!
  2. While loop draws - still brute force, but in a while loop for code brevity
  3. 14 labels (1 per line) with embedded tiles - one label for each line, each label spanning the length of the screen, and using {embed:tile name} to embed tiles into the labels
  4. 14 labels (1 per line) with text tiles - as above, but without the embeds (we'll get back to that)
  5. 1 label (line wrapped) with text tiles - as above, but using line breaks

(3) sounded like it might be a clever solution in the event of label being more performant than draw. (4) you might wonder what the point is in comparing - but in a flash of inspiration it occurred to me that as the total number of tiles I need to draw the maze in first-person is small, and the font character table is editable, I might be able to edit the font directly. A very silly idea!

Results of testing effective full screen draws on every frame with different approaches:

  1. Brute force draws (375 draws) = 7 fps
  2. While loop draws (1 draw looped over all 375 coords) = 3 fps
  3. 14 labels (1 per line) with embedded tiles = 8 fps
  4. 14 labels (1 per line) with text tiles = 16/17 fps
  5. 1 label (line wrapped) with text tiles = 18 fps

(I had some test code wrapping these approaches so I could quickly cycle through them on device without loading a new build, but I assumed the overhead there to be minimal!)

There it was - the tantalising taste of performant full-screen updates! It might seem odd but editing the font to use text characters directly rather than embedding or drawing other tiles gives a huge increase in performance! There are maybe some other useful results there, like knowing while should be avoided when drawing.

Anyway, my font ended up looking something like this:

Any text in the game would be limited to upper case characters, but I could live with that!

Of course I wasn't done with optimising. One big and obvious improvement was to separate out the calculation of what needs to be drawn from the actual drawing. The calculation itself is a very heavy piece of code and running it every frame is a complete waste of resources. I changed it so that the calculation of what to draw happens only when moving (or rotating) and this performed a lot better!

The drawing itself could also be improved. While the tests above are for a complete full-screen redraw, my maze layouts mean I don't actually need to label across the entire screen - especially at the top and bottom of the screen there is a lot of whitespace that always remains whitespace. There was a bit of balance here between the optimal labelling and having manageable code to generate it. I ended up with a somewhat convoluted middle ground where the central rows have the most calculation logic with full row labels with decreasing complexity as you get higher/lower (thanks to less overlapping coordinates in the view).

With all of those changes made I ended up with something actually performant! Not perfect (even by Pulp's 20 fps definition of perfect) but perfectly playable, which made me happy enough!

Please do check out the game - the demo/free edition includes the main versus multiplayer mode. I hope this dev post is interesting, especially as a conclusion to the year-old experimentation above back when I didn't even have my handheld!

9 Likes

Bought! I'm absolutely thrilled this became a full product. Very excellent work

1 Like

@orkn I haven't stopped thinking about this over the week.
It's SO clever!

I love how you have been able to be so creative with Pulp.

Congrats once again on the release!

1 Like