[WIP] Dark Under - Dungeon Master clone prototype

Hello everyone!

I’ve spent some time working on something maybe more approachable for a first timer than the game I presented in the other thread.

I’ve always wanted to make a Dungeon Master/Ultima Underworld (or more recently, Legends of Grimrock…). A while ago I contributed some graphics to a high end product of similar gameplay, but it never got built.

So I decided to give a shot at coding it. Since I still don’t know C++, I used Processing. So far, the filesize is 19.6K including my code and the GFXs. I’ve read the board and it seems a lot of you prototype their projects similarly with javascript, etc.

screen

I’ll post a video and screens at the end of this post. I still need to code the fight system, and most annoyingly wrap my head around how to approach the pivot left/right problem.
I have several questions:

  • Enemies: should they move? I could have them attempt to follow the player or stay static. if I use a quadrant base navigation, it should be straight forward.
  • For those of you that enjoy that type of game, what usually drive you to play them? solving a complex maze? collecting stuff? My level is just a very simple gameplay zoo for testing purpose right now, but I want to know how complex and big I need to get. (no random maze generation, it’s beyond my skillset)
  • Being at 19.6K right now, still missing images for enemies (I doubt I’ll be able to animate them), should I worry? Am I dangerously close to the limit? I may be able to optimize a few things on my splash page and UI and claim so KB back.
  • About the pivot left/right then move forward, it may create some weird disconnect with the minimap (that is always pointing north). Do you think it’s a problem?
  • Any one interested taking my code and helping translate it into C++? I plan to learn it, but I’m also impatient and I know it would go faster if someone could take my source and port it to C++. I believe my code is both clean and simple. I wouldn’t mind a collaboration on this.

Anyway, here’s a video I recorded of my prototype (I made it 4 times bigger than the 128*64 screen for testing/recording purpose, but that’s just 2 lines of code to change it back to the original format.
Video:

Screens:

8 Likes

for people wondering what Processing code looks likes, here’s my level class (I’ll change it later on when I need to load several levels:

class Level {
int originX=108;
int originY=18;
String levelDescription=“Hallway of \nthe dead”;
int [][] worldGrid={
{1,1,1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,0,0,0,1},
{1,0,1,1,1,0,0,0,0,1},
{1,0,1,0,1,0,0,0,0,1},
{1,0,0,0,1,0,0,0,0,1},
{1,1,1,0,1,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,1},
{1,0,1,1,0,1,0,1,0,1},
{1,0,0,1,0,1,0,1,0,1},
{1,1,1,1,1,1,1,1,1,1}
};
Level(){
row=8;
col=4;
myHero = new Player(row, col);
myObjects.add(new Object(3, 3, ‘P’));
myObjects.add(new Object(4, 6, ‘P’));
};

void worldDrawGrid() {
rectMode(CORNER);
for (int row=0; row<worldGrid.length; row++) {
for ( int col=0; col<worldGrid[row].length; col++) {
switch(worldGrid[row][col]) {
case 0: //floor
fill(0);
break;

    case 1: //wall
      fill(255);
      break;

    default:
      println("exception in worldDrawGrid!");
      break;
    }
    rect((col*TILE_SIZE+myHero.originX)-((pcolX*TILE_SIZE)), (row*TILE_SIZE+myHero.originY)-((prowY*TILE_SIZE)), TILE_SIZE, TILE_SIZE);
  }
}

}

}

This looks amazing - cant wait to download it and give it a try :slight_smile:

Agreed … looks awesome.

Excellent game.
I never got round to finishing it or starting the sequel (which I have bought), but I loved it when I played it and I really loved their Lua-based level designer/scripting. I love games that provide the facility for users to create their own scripts/levels.

Probably, else they’re angry statues :P
But that’s something to worry about when you’re far in.

Depends on the feel of the game really.
There’s usually at least some story element underpinning things.
Personally I like puzzles so trap mechanisms and the like are always fun, especially when I can avoid defeating a monster manually by using my wits instead.

You should note that those file sizes aren’t representative of the amount of space the resulting code will use on the Arduboy. For example, some of those images are gifs, the Arduboy does not store its images in conventional formats, they are converted to arrays of bytes that store only the pixels and possibly the size, there is no metadata like you would find in a gif file.

I’m not sure what this pivot problem is.

I’d love to take a look at it but I can’t collaborate or promise anything in regards to translating it at the moment because I have a more urgent project that needs my attendance.

Interestingly enough me and @noel_abercrombie were working on a project like this many moons ago (that slowly shrivelled and died for reasons).

I will say though, C++ can’t necissarily be rushed, you have to learn it one step at a time because it’s a very deep language. You can reach a level where you can write simple things fairly quickly if you already have knowledge of any similar language, but being able to truly squeeze power out of it requires a deeper understanding (of the language theory, hardware and the C++ language itself) that takes time to learn.


The art is very good, though I fear you will hit the memory cap early on if you don’t find some way to compress images or something.

Your processing code seems to be similar to C++, but with some notable changes that make it more akin to C# or Java.

I hope String and new in these cases don’t mean what they mean in normal C++/Arduino code, else that’s going to be crunching far more memory than it needs to be.

I presume the third argument to the constructor of Object is intended to indicate the type of item?

Nice to see a switch statement being used.

1 Like

@Pharap

You should note that those file sizes aren’t representative of the amount of space the resulting code will use on the Arduboy

From your experience, is it usually more, or less? (I would assume less, but worth checking)

I’m not sure what this pivot problem is.

Right now, your ‘vision’ area is always facing north: pressing left means moving left laterally. in these games usually, pressing left means rotate your character left. This means that the orientation of the visual area will become different than the orientation of the minimap.
And there is the fact that I’m struggling to find a way to efficiently treat the rotation visual render: right now I render the preview area by looping in my map array and looking at what’s in row-3, row-2 and row-1 (for the front walls, for example). Since I don’t want to rotate my array, I need switch-cases that look at orientation first, then build the preview for that orientation.
This is my code so far (just a portion of it)

void playerVision (){//draw the walls by checking row and cols ahead of player
image(visionBack,myVision.originX,myVision.originY);
//far front wall
if (row-3>=0){//make sure we don’t check row out of bound in the array
if (myLevel.worldGrid[row-3][col]>0) {
image(farWallFront,originX,originY);
}
}
//far left wall
if (row-2>=0){
if (myLevel.worldGrid[row-2][col-1]>0) {
image(farWallLeft,originX,originY);
}
}
//far right wall
if (row-2>=0){
if (myLevel.worldGrid[row-2][col+1]>0) {
image(farWallRight,originX,originY);
}
}
(etc…)

I’d love to take a look at it but I can’t collaborate or promise anything (…)

Well, it’s not like I’m fighting people off that project anyway :slight_smile: so what I’ll do is finish the processing project, if you can help, cool, if you can’t oh well, I’ll either have to try myself, or move on to something else, no arm done… Trying to stay under 30K and make a proper game, even in Processing is a fun intellectual challenge anyway.

C++ can’t necessarily be rushed

Yep, I get that after looking at some samples and tutorials. That’s why I thought it would be clever to get the prototype first with a language I know… at the very least, I’ll still have a game.

About the enemies. So tonight I had a bit of time. I built an abstract enemy class that includes some random movement for enemies. that class alone weighs 3K.
On top of that, I built a “stalker” enemy that override the parent movement, tracks the player and aggressively pursue them. This extended class weighs 4.27 freaking K!!! Now my whole code weighs 27.3K and I’m getting worried.

I’ll post a video in a short while. Going to have to move at a slower pace: this week is very intense for me and I’m going this week end to the Orlando Indie Galactic Game Jam, so no work this week end for me :slight_smile:

Anyway, will keep people posted - if any contributor wants to step in, please feel free

Oh, missed this:
String is the datatype for strings of characters. As in
String mySentence = “this is a sentence”;

new is to create an instance of an object, as in:
Level myLevel;
void Setup(){ //only once
myLevel = new Level();
}
void Draw(){ //loop that gets repeated
myLevel.worldDrawGrid();
}

this is the first pass for enemies (they don’t show in the “preview” area, but look for the moving X in the minimap) - oh, and disregard the music, it’s not in game, I didn’t realize it was also recording what I was listening to :slight_smile:

1 Like

The amount of memory taken will almost always be less than any actual image file you produce because pretty much every standard image file format in modern use has at least some metadata. Even quaint old .bmp.

The Arduboy images are stored as an array of bytes starting width the width and height of the image and following this pattern:

image

(This image was made by @emutyworks, it’s a very good visual reference for explaining the format.)

Ah.

I’d suggest you pick your row/column offset values based on your character’s orientation.

If your player is facing north, look at tiles in the -Y direction, if your player is facing east, look at tiles in the +X direction et cetera.

Sort of like:

enum class Direction : uint8_t { North, South, East, West };

class Player
{
//...
Direction direction;
//...
};

Player player;

void drawWalls(const Player & player)
{
  switch(player.direction)
  {
    case Direction::North: drawNorth(); break;
    case Direction::South: drawSouth(); break;
    case Direction::East: drawEast(); break;
    case Direction::West: drawWest(); break;
  }
}

And obviously drawNorth would do what you’re currently doing, and the other three would do the same for the other directions.
Something like that at least.
(I’ve erred more on the side of readability than saving memory).

I’ve always got time to make suggestions about how to tackle problems or to explain things about C++, it’s just actually sitting down and writing working/tested code that I wouldn’t have time for at the minute.

You’ll find that much easier once you’ve gotten to grips with C++.
The design of C++ is very much about putting the programmer in charge of how the code does things (e.g. how it handles memory, how big things are, how efficiently things are done).
However that’s a double edged sword: it then means the programmer is actually responsible for having to consider those things - you can’t ignore fine details.

To clarify, that’s mostly the weight of the actual ‘instructions’, which is usually a one-time only cost.
Each enemy will only ‘weight’ as much as the amount of data in it (e.g. x, y, width, height).

Rather than extending the base class (which can actually end up duplicating the code for all the functions of the base class), for something like that you’d be better off following what’s called the “strategy pattern”. Basically you designate one class to containing the movement behaviour and pass it to the enemy.

// Note: this code is a rough idea, I haven't checked it will compile
// I might have gotten some details wrong

// Enemy.h
class MovementBehaviour; // pre-declaration

class Enemy
{
private:
  MovementBehaviour * movement;
public:
  Enemy(MovementBehaviour * movement);
  void HandleMovement(void);
};

// MovementBehaviour.h
class MovementBehaviour
{
virtual ~MovementBehaviour(void) {} // required
virtual void move(Enemy & enemy);
};

// Enemy.cpp
Enemy::Enemy(MovementBehaviour * movement)
{
  this->movement = movement;
}

void Enemy::HandleMovement(void)
{
  if(movement != nullptr)
    this->movement.move(*this);
}

// StalkerMovementBehaviour.h
class StalkerMovementBehaviour : public MovementBehaviour
{
  void MovementBehaviour(Enemy & enemy) override
  {
    // do stuff with enemy
  }
};

Yes, but in C++/Arduino there’s a String class and then there’s a char * class.
char * is very lightweight, String is much heavier and does a thing called ‘dynamic allocation’ that should be avoided on embedded systems.

In C++, creating an object would just be something like:
Level myLevel = Level()
Adding the word new changes the behaviour to do dynamic allocation - which (again) is usually a bad thing.

I’ve written a simple explanation of dynamic allocation and why it should be avoided if you want to read it, but I won’t post it yet because even the ‘short’ version is long.

@Pharap
Your image explanation makes sense! However, does that mean that it’s either the pixel is lit (white) or not(black) in Arduboy? because that would preclude me from using the background (floor tiles and ceiling) as the lit pixels would show through the black pixels of my walls etc…

Your C++ example are very different from what I’m using, but I think I can wrap my head around your method for the “vision” part, definitely something I will explore!

I found a good soul to help me @filmote

Yes, in processing, there is also a char type. I went the easy way to store sentences, but I guess I could store them in an array, and parse them in a char variable? is that how you would do it to avoid using String?

Not sure what dynamic allocation is :slight_smile: I checked wikipedia, and it seems it’s part of memory allocation? In processing, we don’t deal with that, so new is the only way I’ve ever instantiated objects! If you have a link, I’d be curious to read it, just for my personal edification.

light is ‘lit’. dark is ‘off’.

The msb (left hand side) of the byte ends up at the bottom and the lsb (right hand side) of the byte ends up at the top.

image

Luckily pretty much nobody ever has to edit the image data manually because there are many image converters available, a non-exhaustive list of which can be found here.
(The fact I compiled the list isn’t coincidence - it’s good planning :P.)

For a non-animated image it should take roughly 2 + ((width * height) / 8) bytes. You have to round the height up to the next multiple of 8 though, e.g. a 6x10 image would become 6x16.

The first example is fairly easy, but the second example is quite an advanced technique that does rely on knowledge of some more complicated techniques. The idea itself is simple - making behaviour an object - but the implementation requires knowledge of pointers, inheritance and virtual functions. There is a slightly easier version you could try using a thing called ‘function pointers’, but even that’s still a bit advanced. I wouldn’t recommend trying either until you’re more comfortable with C++ and at the very least understand pointers.

Ah, that explains why he decided to call 1943 finished, he’s found a new game to work on :P.
Trust me, you’re in good hands there.

I don’t know much about processing (for all I know, processing’s String might be a char *), so if you’re working in processing I can’t help you much there.

For Arduboy code the Arduboy2 class has several print functions that print text, in which case you can use the F macro to do text inline, or you can store larger sentences in char arrays in progmem.

For example:

void someFunction()
{
  arduboy.println(F("A line of text"));
  arduboy.print(F("Line number: "));
  arduboy.println((int)2);
}

Or:

const char text[] = "Hello World";

// This tricks the arduboy into thinking a progmem string has been created with the `F` macro
constexpr inline const __FlashStringHelper * AsFlashString(const char * flashString)
{
	return reinterpret_cast<const __FlashStringHelper*>(flashString);
}

void someFunction()
{
  arduboy.println(AsFlashString(text));
}

Hrm, I’m guessing that must mean there’s no way to do dynamic allocation in processing and new is just how you normally allocate things. That is the case in some other languages so it’s understandable, but at least now you know to avoid new in C++.

I’ll PM you my simplified explanation to avoid derailing the thread any more than I already have. Sadly I’m not aware of any single simple article online.

Been pretty busy with classes this week, so not much update, but tonight the wife wanted to work and go to bed early, so I got a bit of time to brush up my D&D bestiary… here are the new enemies:

I’m planning on some more (granted that we have enough room!) such as Slime, Mimics and a Lich…

Most likely, my next task will be to prototype the turn based fight system.

6 Likes

just to show that @filmote is doing an awesome work!
screenshot1

5 Likes

While filmote is integrating the existing content, I’m seeing how we can create more variation with the environment: here’s a much lighter wall tileset (below the existing one):

dark&under3

4 Likes

That does say “King’s thot court” right?
I’m not imagining that?

Screens look amazing this is on the top of want list.

Nope, King’s Thot indeed, though not sure if it will stick, it’s just sample text right now, until I imagine a proper progression scenario, hence what those places really mean/represent!

Thanks! I’m super glad to see it come to life as well!

1 Like

by the way, since this isn’t finished, it may not belong to the games category, but just demo? any thoughts?

I’m about to start the combat system. My original mockup was a bit incomplete, so I thought about the minimum amount of steps I could have to make the combat fun while keeping the UI to a bare minimum. Here’s what I thought of so far.
I could use some feedback.

6 Likes

Just for those following the topic and wondering what’s happening: the game is well underway :slight_smile: All systems are mostly in place, and we are starting to bring level content in.

4 Likes