First person dungeon crawler mini-game

Started by newwaveburritos, Sat 07/08/2021 06:19:09

Previous topic - Next topic

newwaveburritos

I've maybe gotten ahead of myself for a little Easter egg that probably no one will care about but it's a fun idea I've been enjoying implementing. That is, I decided it would be fun to add a game within a game and since mine takes place in the late 80s a first-person dungeon crawler on the home PC seems like the kind of thing you would have been playing.  I've managed to make it work but it seems unnecessarily labor-intensive and I'm repeating a lot of code that maybe doesn't need to get repeated this way.

In the mini-game room the player switched to a little pixel arrow I use to track the player's position on a little maze I have plotted out.  The little pixel arrow is controlled tank style with the arrow keys and moves around a walkable area inside the maze.  I track the direction the pixel arrow is pointed as well as its x and y coordinates on the room screen.  Then, I use those to display the appropriate background image of what corner or hallway of the dungeon the player would be seeing.  Now, I could just do it like this but that's hundreds of permutations even for a relatively short maze.  I have the first tile below which I put together as a proof of concept.  So, I know enough to get myself into trouble but not really enough to do this in any intelligent way.

Code: ags

  if ((player.x==3)&&(player.y==3)&&(CardinalDirection==4)){  //in the room's repeatedly execute
    DrawingSurface *surface = Room.GetDrawingSurfaceForBackground();
    surface.DrawImage(0, 0, 24);
    surface.Release();
  }

  else if ((player.x==3)&&(player.y==3)&&(CardinalDirection==3)){
    DrawingSurface *surface = Room.GetDrawingSurfaceForBackground();
    surface.DrawImage(0, 0, 23);
    surface.Release();
  }

  else if ((player.x==3)&&(player.y==3)&&(CardinalDirection==1)){
    DrawingSurface *surface = Room.GetDrawingSurfaceForBackground();
    surface.DrawImage(0, 0, 22);
    surface.Release();
  }

  else if ((player.x==3)&&(player.y==3)&&(CardinalDirection==2)){
    DrawingSurface *surface = Room.GetDrawingSurfaceForBackground();
    surface.DrawImage(0, 0, 25);
    surface.Release();
  }


As always thanks for your help!

Khris

#1
I've already created a dungeon crawler in AGS so let me give my 2 cents.

First, regarding your existing code, you can eliminate a lot of duplicate code here:

Code: ags
  DrawingSurface *surface = Room.GetDrawingSurfaceForBackground();
  int slot;
  if (player.x == 3 && player.y == 3) {
    if (CardinalDirection == 4) slot = 24;
    else if (CardinalDirection == 3) slot = 23;
    else if (CardinalDirection == 1) slot = 22;
    else if (CardinalDirection == 2) slot = 25;
  }
  surface.DrawImage(0, 0, slot);
  surface.Release();
}


Note that for the first two directions, the slot is the direction plus 20, which means if you change the slot numbers of your images, you can turn these six lines into a single line:
if (player.x == 3 && player.y == 3) slot = CardinalDirection   20;

Since there are going to be more spots on the map that will use the same images as 3;3 the next step is to assign a number to each map coordinate and use that to draw the image. Each spot on the map has four optional exits, which means you have a total of 16 combinations. However the four directions all look the same, so you only need 8 images total (unless you want to draw more than the immediate exits).

You probably also don't need to draw this inside repeatedly_execute, at least for now, but only after a movement key was pressed by the player.

However what I usually do is start with something like
Code: ags
void LoadLevel(int level) {
  if (level == 1) {
    map[ 0] = "####################E#";
    map[ 1] = "#      #           # #";
    map[ 2] = "# #### # #####D### # #";
    ...
  }
}

That way I can make arbitrary design changes and won't even have to touch my drawing code at all (this is known as "separating content and presentation" and an essential guideline for programming in general). Using the y coordinate as string index and the x coordinate as character index, I can read map data easily and determine the exits I need to draw with just a bunch of code that checks the surrounding map cells based on the current cardinal direction.

Or I can do what commercial dungeon crawlers did and compose the 1st person view from multiple sprites, based on map data up to several fields away from the player's position, simply drawing from far to near the individual walls, gates, enemies, etc.

newwaveburritos

Your simplification of my existing code would be perfectly adequate for the task at hand: making a hilarious single level that the player is going to spend a few minutes on before moving on but you're right it's not a great system for doing anything more than that if, for example, I wanted to expand it into anything larger which I may want to do sooner or later.

So, your system is making a certain amount of sense to me but I don't exactly see how you connect the string data to the character data.  Are you using this to construct your map coordinates?

Your dungeon crawler is pretty impressive.  It looks and functions perfectly legit.  What I have cobbled together is way inferior.  I don't suppose you still have the source code for that?  I would very much like to take a look at it.  I'm not sure if it's even rude to ask about a thing like that but it would be helpful. And as always, thanks so much!

Khris

#3
The idea is that  map  is an array of strings. You can now write a function like

Code: ags
int GetCell(int x, int y) {
  // everything outside the maze is wall
  if (x < 0 || x >= map[0].Length) return eMazeWall;
  if (y < 0 || y >= level_rows) return eMazeWall;
  char c = map[y].Chars[x];
  if (c == '#') return eMazeWall;
  if (c == ' ') return eMazeFree;
  if (c == 'E') return eMazeExit;
  return eMazeWall;
}


Next, you check the surrounding fields. I usually store a direction as a two-dimensional vector:  int dir_x = 0; int dir_y = -1; // looking north
Now I need the relative directions:
Code: ags
  left_x = dir_y; left_y = -dir_x; // turn dir 90° left
  right_x = -dir_y; right_y = dir_x;  // turn dir 90° right

I can simply add those to the character's position if they strafe, or use them to replace the main direction if they turn. Similarly, walking forward means adding the main dir to the current position.
If I need to check whether they can walk forward, all I need to do is:
Code: ags
  MazeType mt = GetCell(player_x + dir_x, player_y + dir_y); // MazeType is a custom enum


This gives me whatever is in front of the player.  if (mt == eMazeFree)  they walk forward.

Drawing works in a similar way. If I want the maze cell that's diagonally to the left and in front of the player, all I need to do is
Code: ags
  MazeType left = GetCell(player_x + dir_x + left_x, player_y + dir_y + left_y); // one cell forward, one cell to the left

This basic line will always give me the correct map cell, as long as I update all variables after each step/turn. I could now in theory draw a block of rock to the left third of the screen, composing the view dynamically from multiple sprites.

My dungeon crawler uses a pseudo 3D engine (raycasting) like Doom because I wanted to animate turning. But the point is once I've uncoupled storing and processing the map data from presenting it to the user, I can make arbitrary changes to both the level design and the graphics without affecting the other.

Edit: It was uploading, here's the source code (3.2.1+): https://www.dropbox.com/s/pjvan4kjrpbl3ww/ODOW-src.ZIP?dl=1

newwaveburritos

Okay, I've spent a decent amount of time going over this today and I think I understand more about it now.  It makes a lot of sense.  In the first part, you're making a map which you can easily read and check for the map "data."  This also determines where the player can and cannot move.  Then you have to track player direction and the vector so you can know which image to show.  Or more specifically which three or six partial sprites to show depending on which way you're looking.  Really, you're just getting information about what the player is looking at, so to speak, and thus what it looks like.

But, yeah, this makes a lot of sense that you would want to be able to change things without upending everything.

Is this the enum you mean?  This gets returned by the first function so you know what's where.  Perfect.  Thanks so much for taking the time to write this out and explain it to me and for uploading the code for your dungeon crawler.  It's a really good system you have there.

Code: ags

enum MazeType {
  eMazeWall,
  eMazeFree,
  eMazeExit
};

SMF spam blocked by CleanTalk