Help me with C++ for my game


(Pharap) #303

Glad to hear it.

I appreciate the offer but I’m not sure how well it would work out.
Part of my problem is that I have so many projects on the go that each one only creeps a tiny bit further at a time.
I’m not good at single tasking, I’m too easily distracted by other projects.

Not necessarily.
I tend to spend a lot of time focusing on the details and I can be quite lazy at times.

If you test it, every commit should produce the exact same image on the screen,
so if there’s anything wrong then it was wrong before I started.

I haven’t looked into wang tiles before, but I have looked into dungeon generation.
If I can dig up whatever program that was I’ll tell you how it worked.

I write a lot of ‘throwaway’ projects - I sit down and just play around with code for the sake of solving problems.
I then don’t publish the code because it’s a mess, it just sits on my hard drive and rots.

I’m 90% certain this generated the whole board rather than using hashing.
Most likely it used a stack and backtracking or something, to make sure there’s an open path.

Uncanny valley.

The daft thing is that (like I say), they’re probably using different generation techniques that happen to look the same.

For anything 8 bits or less I tend to do it in my head.
Otherwise, Windows Calculator - programmer mode.

ProgrammerMode

Again, I’d suggest writing either a hash or PRNG so you can use the same thing in both, that way you can get the output the same and you can figure it out from there.

I have no clue what you mean by this, but because I like tweaking things:

for(uint8_t i = 0, j = 1; i < 8; i++) {
	Sprites::drawOverwrite((i * margin) + margin, (j * margin), getRoomImage(static_cast<RoomWallLayoutID>(i)), 0);
}

for(uint8_t i = 8, j = 3; i < 16; i++) {
	Sprites::drawOverwrite((i * margin) - (8 * margin) + margin, (j * margin), getRoomImage(static_cast<RoomWallLayoutID>(i)), 0);
}

Or perhaps even:

uint8_t margin = tileWidth + 4;

for(uint8_t i = 0; i < 16; i++) {
	if(i < 8)
	{
		uint8_t j = 1;
		Sprites::drawOverwrite((i * margin) + margin, (j * margin), getRoomImage(static_cast<RoomWallLayoutID>(i)), 0);
	}
	else
	{
		uint8_t j = 3;
		Sprites::drawOverwrite((i * margin) - (8 * margin) + margin, (j * margin), getRoomImage(static_cast<RoomWallLayoutID>(i)), 0);
	}
}

(Slower, but less memory.)

I wasn’t even thinking about checking for bugs so I wouldn’t have noticed.

Hrm, that does sound a bit better.
In fact, what you could do instead is define a set of free functions a like this:

bool hasWallLeft(RoomWallLayoutID layout)
{
	return ((layout & RoomWallLayoutID::OneLeft) != RoomWallLayoutID::Zero);
}

And then use them on the result of getRoomLayoutAt.

That’s a good illustration of what I mean about separation of concerns and composing simple parts to create more complex behaviour.

I’m still a bit clueless on what the intended behaviour is supposed to be,
but that’s probably partly because I’m tired.
It’s been a long day.


#304

It’s the dungeon generator from the last screenshot I showed you that I’m porting to the Arduboy

I wrote a big explanation with pictures here

room from seeds is just any room from 0 to 15, doesn’t matter which. It might help to show you what the rooms are:

image

Now what generateRoomFromSeed does is pick one of these tiles randomly for every tile that (x%2 == y%2). This double modulo is for a chess board pattern. So, here are some tiles generated from seed:

Now, for each green square, there’s only one possible tile that can fit (the exception are the borders of the square, technically they could be exits).

But one thing you can see, is that all walls are 2 pixels wide Note: the borders outside of the square/area/pic is an exception, as we can’t see tiles outside of it. All walls have the same width, and they are made of 2 tiles. One tile has half the wall, the tile adjacent to it contains the other half of the wall (half the thickness)

What I’m getting on the Arduboy is this (I did this in Paint and this is just an example, not the exact setup):

image

The thin wall should not happen. In other words, wrong tiles get selected. The pieces don’t fit, I don’t get my maze-like dungeon I get on the PC

Maybe the solution is to remove what I don’t understand well (the &= etc) and redo it with a switch like I did in GameMaker, and then re-convert it to the &= |= etc setup

Edit: I found a way to test everything.
Commenting out some if statements and thanks yo my A_BUTTON that turns off rendering the tiles generated from neighbours, I can add one wall type at a time and see which ones cause bugs

RoomWallLayoutID DungeonGenerator::getRoomLayoutFromNeighbours(uint16_t xpos, uint16_t ypos) {
    bool leftRoomHasRightWall = DungeonGenerator::hasWallRight(xpos - 1, ypos);
    bool rightRoomHasLeftWall = DungeonGenerator::hasWallLeft(xpos + 1, ypos);
    bool aboveRoomHasDownWall = DungeonGenerator::hasWallDown(xpos, ypos - 1);
    bool belowRoomHasUpperWall = DungeonGenerator::hasWallUp(xpos, ypos + 1);

    RoomWallLayoutID layout = RoomWallLayoutID::Zero;
	
    if (leftRoomHasRightWall) {
        //layout |= RoomWallLayoutID::OneLeft;
    }
    if (rightRoomHasLeftWall) {
        //layout |= RoomWallLayoutID::OneRight;
    }
    if (aboveRoomHasDownWall) {
        //layout |= RoomWallLayoutID::OneUp;
    }
    if (belowRoomHasUpperWall) {
        layout |= RoomWallLayoutID::OneDown;
    }

    return layout;
}

(Pharap) #305

I’ve solved several layers of the problem.

Issue 1 is that you’re offsetting the values in hasWallLeft etc.

E.g.
You call DungeonGenerator::hasWallRight(xpos - 1, ypos), so you’re asking about the right wall of the tile to the left.

Then in hasWallRight you call DungeonGenerator::getRoomLayoutFromSeed(xpos + 1, ypos), which gets the layout of the tile to the right…

The -1 and the +1 cancel each other out, you you’re basically asking for information about the tile you’re actually trying to fill in.

Issue 2 is that you aren’t checking that xPos - 1 (or xPos + 1, or yPos - 1 or yPos + 1) are valid indices.

You can check that by doing stuff like:

if ((xPos == 0) || DungeonGenerator::hasWallRight(xPos - 1, yPos)) {

And finally, issue 3 is with the tile at (0, 0).
This is because randomSeed(0) does something very particular:

So basically, you either have to treat 0 specially or you get rid of randomSeed and random and use a hash function or other PRNG.
(A PRNG that actually can accept 0 - i.e. not whatever avr-libc is using and not a linear congruential generator.)


Oh, and the second argument of random is exclusive, so to get 0-15, you need to feed it 16 as a second argument.
But that gives you solid blocks in some ‘rooms’, so I can see why you might not want to do that.


#306

I am confused. Why do they cancel each other out?

1100 or 0011 == 1111 in bitwise operations, no?

Also, I’m testing everything in isolation, and so far they’re generating correctly (but I am not done) I seem to be done and if I’m using the 4 walls for example, then all the screen is filled with black rooms. If a use tiles with 3 walls, they also seem to end up correct. No 1pixel wide walls in all my testing

What I did to test is this, and change the room to whichever I want to test:

RoomWallLayoutID DungeonGenerator::getRoomLayoutFromSeed(uint16_t xpos, uint16_t ypos) {
    return RoomWallLayoutID::ThreeRightUpDown;
}

(Pharap) #307

Imagine you are on tile (5, 5).
You call DungeonGenerator::hasWallRight(xpos - 1, ypos),
I.e. DungeonGenerator::hasWallRight(5 - 1, 5)

Then DungeonGenerator::hasWallRight calls DungeonGenerator::getRoomLayoutFromSeed(xpos + 1, ypos)
I.e. DungeonGenerator::getRoomLayoutFromSeed(4 + 1, 5)

So DungeonGenerator::hasWallRight is trying to look at the tile you’re currently trying to fill.

Binary doesn’t even come into it. This is about the coordinates.


I’ve made a PR for my fixes.

If you go onto the fixes branch of my fork you can download the code to check that it fixes the problems.


#308

Wait, I think I know what the problem is

0000 + 1000 can’t work, because I actually need to end up with 0001

I’ll modify this and quickly test my theory

Thanks for talking about the bitwise operations, I think you’re right and the problem lies there
Edit: but wait, I already tried inverting those and it didn’t work
Other edit: I’d have to use bitshifting, I don’t think there’s a single bitwise operation for what I need. Instead I should treat booleans as booleans, and apply the information I get manually to the variable

I’ll still save myself a huge switch statement


(Pharap) #309

I wasn’t suggesting the bitwise operations are the problem.

Remember that you don’t use + for combining bits, you use |.
I’m talking about arithmetic offsets in the tile coordinates.

Stop for a minute and check my PR/fork.
If you know how to get a copy of the code after each commit, try that too,
you’ll see which commit fixes which problem.


I think you might have completely glossed over my previous comment, or missed it somehow:


#310

I was gonna say it’s not possible, then I wrote “wait… You’re making me doubt. I’ll double check” and then you were right.

I’m surprised I didn’t freeze the Arduboy with infinite recursion.
Edit: oh I get it, in the beginning, those function would offset in one direction. Then I changed it, so now they would just cancel each other out. Now I understand

Thank you!! It’s working!

So I guess one lesson here is… if you need to get info from 4 different places, then maybe having 4 functions instead of having a multitasking one, is a good idea.

Another lesson here might be… check your arguments. Check what you’re passing in


#311

You said it clear as day and for some reason it went right over my head. Thank you for your patience
Edit: I meant before, I’ve now understood it

My multiplication and division end up setting 0 as a seed?
I do get not very unique patterns on the left so I definitely think you’re onto something.


#312
Actually I don't understand. Shouldn't it cause infinite recursion?
getWallFromNeighbour(right here)

      right here

      return getWallFromNeighbour(right here)

          right here

         return getWallFromNeighbour(right here)...

(Pharap) #313

Precisely.

Sometimes the key to debugging code is to pretend to be the CPU and to work through the code by hand.

On a desktop computer most compilers have breakpoints and step-through debugging, so you don’t have to do that, you can just use the IDE to step through the code.

But honestly, sometimes I find the fancy tools an IDE provides take too much of the workload, and let programmers get too lazy/reliant on the IDE.

Yep, part of the ‘separation of concerns’ principle again.

If you decide to switch to my suggestion of making your wall checking functions more like this:

bool hasWallLeft(RoomWallLayoutID layout)
{
	return ((layout & RoomWallLayoutID::OneLeft) != RoomWallLayoutID::Zero);
}

That sort of thing would make this kind of bug easier to spot, because there’s less going on.

Yep.

In desktop programming where you’re able to use ‘exceptions’ (if you’ve never encountered them before, don’t worry about them for now) to signal errors, and one of the most common uses of them is to check arguments.

If it had been earlier in the day I might have drawn a diagram.
I find that diagrams are a severly underrated programming tool.
I regularly work problems out on paper before even touching any code, and I find that drawing the problem (e.g. grids, graphs, lines, sprites) can often make it easier to spot the problem.

No, it’s just tile (0, 0).

Here’s the code from before my change:

uint32_t xSeed = static_cast<uint32_t>(xpos);
uint32_t ySeed = static_cast<uint32_t>(ypos);
xSeed = (xSeed*100*100)/1337;
ySeed = (ySeed*100*100)/501;
uint32_t currentSeed = ((xSeed << 16) | (ySeed << 0));
randomSeed(currentSeed);

if xposis 0 and ypos is 0 then xSeed becomes 0 and ySeed becomes 0.

Then you have the chain of multiplication and division.
Any value multiplied by 0 is 0.
0 divide by any value is 0.
So xSeed and ySeed are still both 0.

Then you get to ((xSeed << 16) | (ySeed << 0)), which at this point is ((0 << 16) | (0 << 0)).
Sure enough, 0 shifted either way by any amount is still 0, and 0 | 0 is 0.

So then you end up with randomSeed(0), and if you dig into the implementation you find this:

So randomSeed ignores an argument of 0, which is why I made this change:


No, it’s different functions being called.

getRoomLayoutFromNeighbours => hasWallLeft => getRoomLayoutFromSeed


#314

I do that too! But I’m surprised you’d make one even for someone else.

**I have a slightly unrelated question. How do you handle helping vs. not helping vs. helping a little bit vs. helping a lot, a person? I ask for two reasons:

  1. You could have lost patience when I stopped noticing what you were writing
  2. Some people seem to not want to put efforts in (not googling something, not reading the documentations, etc)
  3. I’ve obviously fallen in category number 1, and I might seem like category number 2 once in a while. Sometimes I’m unable to google something properly (doesn’t usually, but it happens)

How do you manage/react/etc all of this?

As a note, one of the best drawings I’ve ever seen for explaining something was this:

Picture

http://www.chaoticstupid.com/wp-content/uploads/2015/06/aweaponview.png

Source: https://www.chaoticstupid.com/trinity-5-setups/

I’ll take any excuses to share those articles, they’re excellent.

Oh, ok. Makes sense then


(Pharap) #315

A bit of an odd article. Unfortunately I can’t look at a cube without seeing a 3-dimensional spectrum of values.

Like the canonical colour cube:

Sometimes a 5 minute diagram saves an hour of explanation.
Hence it’s in everyone’s interest.

I’ve been told I’m an incredibly patient person.

Perhaps it’s because I’m used to wading through technical documentation?
I’ve seen some exceptionally tedious and frustrating technical documentation.
(I especially hate academic papers full of maths and overly-formal nonsense.)

There’s probably other reasons too.

Ultimately as long as I think someone is putting effort in and genuinely wants to learn/improve then the time it takes them to understand something is less important.

When I lose patience is when people don’t want to put any effort in.
When it comes to programming, the more you put into it, the more you get out of it.

In general I’m willing to offer programming advice to anyone who asks, regardless of experience level or the complexity of the question.
I’ll just as happily explain functions and return types as I will templates and SFINAE.

However,
I’m more ready to help people who demonstrate that they have taken at least some of my advice on board.
Thankfully most people do.

Also I know some people take longer to understand certain concepts than others and a lot of the time the way something is explained can make a difference.

Part of my motivation is trying to get people to write what I believe to be good code.
Even if I can only convince people to adopt one or two things
(e.g. constexpr variables over macros, scoped enums over unscoped enums or defines)
then I’ve made progress.


I managed to find two of the three programs I mentioned earlier.
I might publish them later, but at the moment they’re very old and very ugly.
One is just a well executed drunken walk.

The one that produces patterns similar to what I’ve been helping you with is actually very different to the wang tile approach.
That one really would best be explained with a diagram.


Lastly, I haven’t made a PR yet, but I threw a few more improvements up to a local branch so you can decide which (if any) of the changes you like, then I can make a PR tomorrow.

That’s it for me for today. It’s very late here.


#316

I’m not sure what you mean by PR

Have a great night! Thanks for all the help


(Pharap) #317

Pull request.

It’s something that happens so often in git that people just shorten it to ‘PR’ most of the time.


When you get chance, let me know which of my changes you like and which you don’t,
and then I’ll put them toegether and make a PR.


#318

I’m a bit confused because on this page on Github (which I’m not sure if you can see) it seems like some parts of the code isn’t showing.

In case you can’t access the page, here’s a screenshot

Screenshot

Just a quick note, some elements were just quickly hacked in to be able to test more quickly, like the A button check and the B button check to not draw everything

I thought I had accepted your changes yesterday (all of them) but maybe I didn’t? It says “This branch is even with TheProgrammer163:master.”

Screenshot

I’ve been doing some thinking, and I can make my RPG/Hack N’ Slash step by step. All the thing I want to add in can almost make a game on its own. Random dungeons with enemies? That could be one game. Open world with exploration like Zelda? That could be its own game too.

I can make the game little by little focusing on one thing at a time and have a fun game from the very beginning.

I’ll be taking it slow for the next few days, I went pretty intense on programming recently.


(Pharap) #319

That’s because it’s looking at just one commit

It seems like for some reason the onebox system on Discourse redirects to the latest commit.

It was supposed to link to here.

Each commit only displays a small part of the changes.
If you look around there’s a ‘view file’ button that will show you the whole file at that point in history.

If you’re seeing that then you’re looking at the master branch.
If you press the grey button above that message that says ‘Branch: master’ it will should you a list of other branches that you can switch to.

I put my later changes on the improvements branch.

I’d call those ‘demos’ more than games.
It would mostly work, but you’d undoubtedly end up with some overlap.

Fair enough.

It’s been quite some time since I’ve had a proper break too.
(Though I did cave in yesterday for a while. I messed around on Garry’s Mod for about an hour.)


#320

Spends a 18 days helping some new programmer 24/7.
Spends 1 hour playing a game.
Calls it “caved it”

I see it now. Putting RoomLayoudId and its operators in a seperate header file is great.

I’m confused about why you added in a second const keyword here

Everything is good. I have a foggy memory so I wasn’t sure if I had uploaded the last version I had, so I did it just now. If your version has the same result as the version I uploaded now then this means they both work correctly and I’d merge all your commits


(Pharap) #321

Fair point.

A lot of people get scared when they see lots of files because they think “so many files to keep track of”,
but I get scared when I see a long file because I think “so much scrolling”.

Personally I think that if you have a lot of smaller files then it’s easier to find things.
For one thing it’s similar to the idea of having lots of small functions instead of just a few large functions.
Also I think searching for a specific filename takes a lot less time and effort than scrolling through one massive file.

And the other bonus is that if you keep your code in lots of small, modular files, it’s easier to reuse them in other projects because you can move the file instead of trying to extract a big glob of code.

Originally I was going to wait to teach you about this, but I couldn’t resist doing it now.

I’ve got to head off, I’ll explain when I get back.

If you’re happy with everything I’ll make a PR.
I prefer making a PR because it makes it easier to keep track of things.


(Pharap) #322

Ok, so, the const on the end of a member function declaration is called a const-qualifier and it turns the member function into a const-qualified member function.

What this means is that you can call it even when an instance of the class is const.
If it didn’t have the const qualifier, you wouldn’t be able to call it from a const instance.

This is best demonstrated with some code:

class SomeClass
{
private:
	int value;
	
public:
	SomeClass(int value) : value(value) {}
	
	int getValue()
	{
		return value;
	}
};

int main()
{
	SomeClass a = SomeClass(5);
	
	// Works fine
	int aValue = a.getValue();
	
	const SomeClass b = SomeClass(6);
	
	// Error, cannot call non-const-qualified member function
	// from a const object
	int bValue = a.getValue();
}

When getValue isn’t const-qualified, you can’t call it when the object is const.

However, if you const-qualify it, you can:

class SomeClass
{
private:
	int value;
	
public:
	SomeClass(int value) : value(value) {}
	
	int getValue() const
	{
		return value;
	}
};

int main()
{
	SomeClass a = SomeClass(5);
	
	// Works fine
	int aValue = a.getValue();
	
	const SomeClass b = SomeClass(6);
	
	// No error, getValue is const-qualified
	int bValue = a.getValue();
}

It is common practise to make all ‘getter’ functions const-qualified so they can be used when the object is const.


When you specify two member functions with the same name, but one is const-qualified and one isn’t,
the const-qualified version is called when the object is const,
and the non-const-qualified version is called when the object isn’t const.

class SomeClass
{
public:
	void printMessage()
	{
		std::cout << "I am not const!\n";
	}
	
	void printMessage() const
	{
		std::cout << "I am const!\n";
	}
};

int main()
{
	SomeClass a;
	
	// Prints "I am not const!"
	a.printMessage();
	
	const SomeClass b;
	
	// Prints "I am const!"
	b.printMessage();
}

In your case, I have done this with getRoomLayoutAt.
The functions have the same implementation, but the const-qualified version returns a const reference instead of a normal reference.

This means that for a non-const dungeon you can do this:

dungeon.getRoomLayoutAt(x, y) = someValue;

But for a const dungeon, that becomes a compiler error
(because you can’t modify the object referenced by a const reference).


If you want to know more about this sort of thing, the idea is called “const correctness” and isocpp have a very good FAQ about the subject:

https://isocpp.org/wiki/faq/const-correctness

If this is all too complicated for you to worry about at the moment, then don’t worry about it.
Const correctness is another advanced topic that I like to throw at people without warning.