First Person "3D" Maze & Relative Movement

Design a maze in 2D in the pulp editor, then walk around it in first person!

first_person_maze_demo
(you may need to click the gif for it it play)

The player remains on a single screen visible in the regular top-down perspective. Press the "A" button to toggle the first-person perspective, which is achieved by drawing over the screen in the player's draw event. Any solid tiles are assumed to be walls, while any non-solid tiles are assumed to be empty space.

Player movement is also overridden from the default absolute movement, where the keys correspond to the cardinal directions, to be relative movement, where up/down move the player forward/back with respect to their orientation, and left/right rotate the player 90 degrees anti-clockwise/clockwise respectively. This allows for natural movement in first person, but it might be useful in a top down game anyway!

All corridors should be 1 tile wide, no open areas (they will be playable but display strangely).

Here's the json if you want to try it out:

First Person Maze.zip (5.6 KB)

If anyone is interested in how any of it works, feel free to ask. Otherwise the code is hopefully relatively simple to follow, although I'm sure it could be tidied up and cut down a fair bit!

15 Likes

That's so cool! Awesome work there!

1 Like

Thanks!

I've tweaked the display, it's now in a better perspective:

first_person_maze_demo_2

First Person Maze.zip (5.5 KB)

There are still a few improvements I want to make to the basic display and then I might try and make a simple game out of it.

2 Likes

This is awesome!
I wonder if I can recreate this effect in my e-motive game... It's not exactly a maze, but they're will be walls and stuff on the floors like hazards and artifacts (items)... Could make for an interesting extension of this effort :slight_smile:

1 Like

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:

8 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

4 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:

2 Likes

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)

3 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