Help me with C++ for my game


#223

Well it’d be 6 extra images (masks included), so it’s be 48 extra bytes. I guess you’re right

So apparently the reason that I hit an invisible wall when I get far to the right, is because my function Level::isTileOutOfBoundsAtTile(xIndex,yIndex) thinks it’s outside the level. I know this because I replaced the out of bounds tile from solid to empty, and now instead of having an invisible wall, I just fall through the floor.

I’m curious if this is because we calculatet the maximum value of X wrong, and it ends being far in the negative.

Edit: I removed that out of bounds check, and strangely enough it really is out of bounds. Without it, wether a block is solid or empty is completely random. What I don’t understand is why, if it is out of bounds, that it renders correctly.


(Pharap) #224

I had a look and a think, and I think these are wrong:

bool Level::isIndexOutOfBoundsAtTileX(int16_t xIndex) {
    return ((xIndex < 0) || (LEVEL_WIDTH_IN_TILES <= xIndex));
}
bool Level::isIndexOutOfBoundsAtTileY(int16_t yIndex) {
    return ((yIndex < 0) || (LEVEL_HEIGHT_IN_TILES <= yIndex));
}

If I invert the logic to the ‘variable on the left’ style, you get this:

bool Level::isIndexOutOfBoundsAtTileX(int16_t xIndex) {
    return ((xIndex < 0) || (xIndex > LEVEL_WIDTH_IN_TILES));
}
bool Level::isIndexOutOfBoundsAtTileY(int16_t yIndex) {
    return ((yIndex < 0) || (yIndex > LEVEL_HEIGHT_IN_TILES));
}

That should be >= rather than >.
So basically, you’ve got an off-by-one error which means you’re going outside the bounds of levelData.

I.e. it needs to be changed to:

bool Level::isIndexOutOfBoundsAtTileX(int16_t xIndex) {
    return ((xIndex < 0) || (LEVEL_WIDTH_IN_TILES < xIndex));
}
bool Level::isIndexOutOfBoundsAtTileY(int16_t yIndex) {
    return ((yIndex < 0) || (LEVEL_HEIGHT_IN_TILES < yIndex));
}

#225

I thought so too but there’s 32 blocks wide that’s not working (I compared with my PyxelEdit tilemap and counted)

But the weirdest thing is that it renders properly. I can see the rest of the level, it just doesn’t behave properly. I’ll go compare my collision code with my rendering code, maybe I’ll find something

I thought maybe the x and y were looping around but the view would move if that was the case, and the player too. I check anyway just in case and the x, with print that you suggested which is great, show normal x coordonates.

Edit: in both cases I use getTileAtTile, but in one case it’s with i and j from a nested for loop, and in the other it’s with xOnGrid and yOnGrid… but those seem to work fine up until that point

I’ll multiply i and j by * and pass them in xOnGrid to see if it gives a different result

Edit: this is how I’m gonna test if the source of the problems are the xOnGrid and yOnGrid functions

    //Sprites::drawSelfMasked((i-xStart)*LEVEL_TILE_WIDTH-xOffset, j*LEVEL_TILE_HEIGHT, Images::level, tile);
    int16_t drawI = Level::xOnGrid(i * 8);
    int16_t drawJ = Level::yOnGrid(j * 8);
    Sprites::drawSelfMasked((drawI-xStart)*LEVEL_TILE_WIDTH-xOffset, drawJ*LEVEL_TILE_HEIGHT, Images::level, tile);

If I see crap instead of the level, then the onGrid functions are the problem

Edit: It still renders correctly… I am so confused. This below, draws the whole level. No junk anywhere:

// draw the world
void View::draw() {
    int8_t xOffset = (this->x - ((floor(this->x / LEVEL_TILE_WIDTH) * LEVEL_TILE_WIDTH)));
    int16_t xStart = this->x/LEVEL_TILE_WIDTH;
    for(uint8_t i = xStart; i <= viewWidthInTiles+xStart; i++) {
        for(uint8_t j = 0; j < viewHeightInTiles; j++) {
            uint8_t tile = Level::getTileAtTile(i, j);
            //Sprites::drawSelfMasked((i-xStart)*LEVEL_TILE_WIDTH-xOffset, j*LEVEL_TILE_HEIGHT, Images::level, tile);
            int16_t drawI = Level::xOnGrid(i * 8);
            int16_t drawJ = Level::yOnGrid(j * 8);
            Sprites::drawSelfMasked((drawI-xStart)*LEVEL_TILE_WIDTH-xOffset, drawJ*LEVEL_TILE_HEIGHT, Images::level, tile);
        }
    }
}

This brings junk collisions, but only at LEVEL_WIDTH_IN_TILES - 32 tiles

bool Level::detectWall(int16_t x, int16_t y) {
    int8_t headProtection = 4; // 3 is also a good number
    int8_t x1 = xOnGrid(x);
    int8_t x2 = xOnGrid(x+LEVEL_TILE_WIDTH-1);
    int8_t y1 = yOnGrid(y+headProtection);
    int8_t y2 = yOnGrid(y+LEVEL_TILE_HEIGHT-1);
    
    return (getTileAtTile(x1, y1) != 0 || getTileAtTile(x2, y1) != 0 || getTileAtTile(x2, y2) != 0 || getTileAtTile(x1, y2) != 0);
}

The onGrid functions aren’t the problem, at least we know that.

This is my getTileAtTile function. the if (yIndex >= 7) return 1; is just to have a floor everywhere for testing:

uint8_t Level::getTileAtTile(int16_t xIndex, int16_t yIndex) {
    /*
    if (Level::isTileOutOfBoundsAtTile(xIndex,yIndex)) {
        return 0;
    }*/
    if (yIndex >= 7) return 1;
    return pgm_read_byte(&(levelData[LEVEL_WIDTH_IN_TILES * yIndex + xIndex]));
}

Edit: these two return the correct values, and it’s the exact same code used to get the x and y for the collision box:

arduboy.println(x.getInteger()-4);
arduboy.print(y.getInteger()-4);

(Pharap) #226

It doesn’t solve the problem, but I’d like to point out that floor is completely unnecessary when calculating xOffset in draw because x is an int16_t (i.e. an integer type - no decimal point or fraction to consider) and imathematical operations on core types always get promoted to the most suitable type (which in this case is int).

Using floor actually forces promotion to double because floor is double floor ( double __x ), so the surrounding expression ends up being double too because it gets promoted.

After cutting it out, the resulting expression ((this->x / LEVEL_TILE_WIDTH) * LEVEL_TILE_WIDTH)) could be simplified to this->x - (this->x % LEVEL_TILE_WIDTH).

That in turn gives you:

int8_t xOffset = (this->x - (this-> x - (this->x % LEVEL_TILE_WIDTH)));

And the two subtractions cancel each other out (e.g. (10 - (10 - (4))) == 4), leaving you with:

int8_t xOffset = (this->x % LEVEL_TILE_WIDTH);

#227

Do you have any ideas why what is drawn to the screen doesn’t correspond to the collision, and only at the end of the level - 32 ?

It’s quite interested that it’s a difference of a power of two and a power of 8, it’s as if the wrong bytes were being read… but only for the collision detection. I even added a function to put the camera at the end of the level, and it renders 100% correctly, any ideas?

Edit: thanks for the modulo tip, it’s much cleaner code


(Pharap) #228

I found your problem.

Broken:

bool Level::detectWall(int16_t x, int16_t y) {
    int8_t headProtection = 4; // 3 is also a good number
    int8_t x1 = xOnGrid(x);
    int8_t x2 = xOnGrid(x+LEVEL_TILE_WIDTH-1);
    int8_t y1 = yOnGrid(y+headProtection);
    int8_t y2 = yOnGrid(y+LEVEL_TILE_HEIGHT-1);
    
    return (getTileAtTile(x1, y1) != 0 || getTileAtTile(x2, y1) != 0 || getTileAtTile(x2, y2) != 0 || getTileAtTile(x1, y2) != 0);
}

Fixed:

bool Level::detectWall(int16_t x, int16_t y) {
    int16_t headProtection = 4; // 3 is also a good number
    int16_t x1 = xOnGrid(x);
    int16_t x2 = xOnGrid(x+LEVEL_TILE_WIDTH-1);
    int16_t y1 = yOnGrid(y+headProtection);
    int16_t y2 = yOnGrid(y+LEVEL_TILE_HEIGHT-1);
    
    return (getTileAtTile(x1, y1) != 0 || getTileAtTile(x2, y1) != 0 || getTileAtTile(x2, y2) != 0 || getTileAtTile(x1, y2) != 0);
}

(This was actually one of the changes I was going to suggest earlier, but I wasn’t expecting it to be causing an issue this early on.)

Now I can go and eat my dinner in peace.
I didn’t want to go until I’d solved that. :P


As much as I hate maths, it really pays to know how to be able to figure out which operations cancel each other out.

Also, the modulo operator is used a lot more in games than other applications, so it pays to be aware of it and understand how it works (in particular, how it behaves with signed values).


#229

Can you put in bold the parts you changed? (maybe you’re already doing it, I noticed you’re editing it as we speak)

Edit: Hum I think you might have accidentally pasted the broken code twice lol. Either that or my eyes are playing with me again (I did paste it over my code to make sure)

EDIT: It actually works! I’m confused, I’ll have to read it again to find what you changed

Edit: ohhhhhhhhhhhhhhhh it overflowed because of the int8_t… hahahaha. Well that explains it. I had the right idea about checking if there was an overflow, but I didn’t check in the right places

Thank you so much. I think I used int8_t because I initially wanted to use them unsigned and changed my mind

My levels are 160 tiles wide, I gave myself a budget of 8500 bytes for the levels. If I want to have a ton of them I’m gonna have to to either compress them really well, maybe with run length encoding like you’ve suggested before, or I’m gonna have to use either 2 tiles or 4 tiles. With 4 tiles I’d get about 25 levels if my memory is correct

I could have 1 byte per column, the levels would literally be sprites compatible with the Arduboy. Just 0s and 1s. I’d have 53 levels this way for 8480 bytes. I guess I could go higher than that, 10240 bytes is what my uncompressed 160*8 level is currently taking Little edit: this includes coins, spikes and entities and anything else I decide to add. In the case of 2 tiles and 4 tiles entities would be stored seperately… but I think I won’t have any enemies. A platformer with a timer and a rating based on how quick you finished a level should be fun enough.


(Pharap) #230

Originally I was just going to tell you detectWall was broken and see if you could figure out why on your own, but I decided that might be a bit harsh.

Anyway, the moral of this bugfix is to always check the types,
and to try to use explicit casts when you perform a narrowing conversion.
Widening conversions (e.g. int8_t to int16_t) are rarely an issue,
but narrowing conversions (e.g. int16_t to int8_t) always mean a loss of data.
(This is why I chose to make the fixed point types require explicit casting - it makes it easier to spot where data is being lost.)

Also, consider getting a ‘diff’ program. :P
(Or you could have just branched your repo, pasted the ‘corrected’ version and then looked at the commit to see what the difference was - something to bear in mind.)

int8_t is signed, not unsigned.
(uint8_t is unsigned.)


But how high? 8?

That’s possible as long as you can fit it in RAM.

Given the way your world works, I think it would make sense to RLE vertically rather than horizontally, so it’s easier to scroll columns in and out.

If you did RLE for columns, assuming 5 bits for tile type and 3 bits for length, or 4 bits for type and 4 bits for length then the worse case scenario would be an alternating pattern (10101010), so that’s 8 bytes per column.
The average case (based on the level you’ve already made) would be 2 or 3 bytes per column.

If you were doing 16 tile types, without RLE that would be 4 bytes per column, and if you were doing 4 tile types that would be 2 bytes per column.so whether RLE makes sense depends on how complex your levels end up being.

If you were having 4 tile types and you packed two RLE blocks per byte (i.e. 2 bits type, 2 bits length, 2 bits type, 2 bits length) then you’d have about 1-2 bytes on average and 4 bytes in the worst case, so that might work out alright.

It might even pay to make some levels use RLE and some to just be encoded normally, so you could pick what works best.

You could do, although the logic for extracting the bits would be a bit awkward.

If you collect coins then that makes them mutable, so those should probably be entities rather than tiles.

Spikes would probably be tiles though.


#231

If I have coins I’d need to load the levels in RAM since they can be modified, and if I don’t use many tiles it would be possible. If I use 2 bits per tiles and end up using only 3 different tiles the’re some waste there. Animating coins would be easy; using a global timer and changing the image for drawing coins. Also if I put coins everywhere, it’d become costly quickly, so they’d be better as a tile (I actually did all this already in GameMaker, I’m using tiles for everything)

I’ll probably clean up the code I already have and when it’s ready I’ll start playing with different ways of handling levels

I also thought about storing my levels as a set of boxes with a width and height. It’d be one byte for the dimensions (5 bits for the xcsale, 3 bits for the yscale). one byte for the x, 3 bits for Y and 5 bits for the tile type/entity type. I could convert it into tiles and entities in RAM. I’d have a limit of how many boxes I can have per level, and it would give less freedom

Can’t be worse then autotiling for a 16tiles tileset. Not sure if it’d be quick enough to use in real time however. Not for collision detectiong but for rendering, there’s 17*8 tiles, in view, that’d be 128+8 calls to that function, meaning I’d probably have to load it in RAM first


(Pharap) #232

Not the tiles themselves, just a list of entities.

Yeah, but you can’t fit 3 tiles in 1 bit. They’re bits, not trits.

A list of coins wouldn’t eat as much as packing your whole level into RAM, but there would be a point at which a list of coins would be heavier than packing a part of the level into RAM.

The question would be what that point is and how likely you are to hit it.

Also, if you loaded only part of the level, you’d have to prevent backtracking so people couldn’t trick the world into producing infinite coins by continually backtracking (forcing the buffer to reload the tile data as if the coins hadn’t been collected).

I’m still not sure what ‘autotiling’ even is.


#233

It’s when you have different sprites for a single tile type. For example I could have 16 different tile images for my walls, and instead of storing them in the level, they’d all be stored as whatever number represents the whole.

When loading the level, the right image is found based on the tiles around it.

The walls in my game are just 0s, the right image is found automatically https://theprogrammer163.itch.io/arduboy-hack-and-slash-adventure

I’ll do a simpler version of that for my platformer; any tile at yIndex 7 will be a floor tile instead of a while tile.

I might do something like that for spikes too; they would just stick to the ground if there’s a solid below. None? Solid to the right? Ok stick to the right etc.

I’d have coins everywhere. For game design/level design hey’re really useful to tell the player where to go without being obnoxious, and they’re also useful as a goal for the player

Hum good point.

If I add a time limit it probably wouldn’t matter, unless ‘collect all coins’ is one of the goals for level completion. Although, how many coins can you get in X amount of time can also be an interesting goal. There’s a lot of good options on handling this,

Yep that’s why using the 4th tile for coins would work well, I’d use it all.

Spikes aren’t that necessary since I can just use the void as a death trap, so I might use just 2 tiles + coins separately. So many options


#234

Holy crap this is amazing . I made a really tough level and I’ve been playing for almost an hour straight. I finally beat it. When I made my estimate for wanting 25+ levels, I had made fun, but very easy levels. If I make them hard the total playtime is gonna be way bigger

I used only two tiles; solid and non solid, that’s it. Coins might be useful to guide players but otherwise I really don’t need anything other than a wall to make a fun level.

I thought I’d have to tweak the view, make it go in front of the player a bit like it does in Super Mario games, but I didn’t have trouble seeing in front of me so maybe it won’t be necessary. A moving camera could be annoying. A crouch mechanic would be useful to move more slowly, you need about two blocks to get fast enough to do a high jump, and it’s easy to die from going a bit too far off and just falling of the block. Maybe something that caps the velocity at 1/16, or something

This is gonna be great. This is gonna be a great game


#235

I beat it on my first try this morning, I didn’t check the time but probably in under two minutes. I got a lot better from playing so much yesterday. (speaking of which, I should add the timewatch feature next)


(Pharap) #236

If you plan to add a pause feature at all then make sure the timer is pausable.
(I.e. it accumulates ‘delta time’ rather than relying on ‘wall-clock time’).


(Holmes) #237

This is a really good thread. :hearts: :hearts: :hearts:

A lot of seemingly random concepts that would be good for a beginner to read through to get a little teaser of a lot of different concepts.

Also, @SamSibbens, I love the pixel art.


#238

You made me want to re-read everything, I’m halfway through. I’m quoting good information and putting the direct link of good posts, with a short summary. I’ll do the other half later.

I can edit the original post and add this in, this way others won’t have to read the 235+ posts for the useful stuff


(Pharap) #239

All my comments are useful.

Especially the ones that are pointless jokes and random anecdotes instead of programming guidance. :P


#240

I found an article about Hungarian notation, we recently talked about it so I thought I’d share it with you https://www.joelonsoftware.com/2005/05/11/making-wrong-code-look-wrong/

Apparently the original Hungarian notation was different and actually useful, but people misunderstood and started just adding a prefix of the data type

I’m making a level editor for my game that will output levels as a 1D c++ array. I can already edit the level in-game which is how I was able to test a lot of different obstacles and adjust the jump mechanic in my game, and learn what jumps where possible and testing them, before adding them to my ultra-hard level I made yesterday. If I make a full-fledged version for PC the level editor will be included and people will be able to use it, similar to Forge in the Halo games


(Pharap) #241

I’ve read that before, and it’s a bad argument.
Hungarian notation shouldn’t be used to signify things like ‘safe string’ vs ‘unsafe string’.

That’s the job of the type system! That’s what type safety is for!

UnsafeString name = request("name");
// ...
SafeString safeName = encode(name);

No special coding convention required, just a decent strongly typed language.


If you want to see something really impressive regarding type safety,
watch Bjarne Stroustrup’s “Going Native 2012 Keynote”.

He discusses the idea of implementing SI units as types in order to prevent programming errors like the kind that caused the mars climate orbiter disaster.

(In case the video doesn’t play at the correct time, it’s about 18:52.)

He spends about 9-10 minutes discussing this before moving on to other things.

Unfortunately the full talk is very long because Bjarne talks quite slowly to make sure everyone’s on the same page, but it’s really interesting and helps to give a deep insight into the power and philosophy of C++.

I think there are three main reasons people are afraid to adopt the ‘SI unit’ approach:

  1. They don’t like the amount of extra code that’s required (‘boilerplate’)
  2. They don’t understand enough about types and how type systems work
    • People often hate things they don’t understand
  3. As a result of 2, they assume that type conversions are costly, which isn’t true for various reasons
    • A lot of type conversions for small/simple types only exist at compile time
    • A lot of type conversions amount to just a few copies or a few arithmetic operations
    • When it is expensive, it’s usually not much more expensive than the unsafe approach

#242

I was curious what you’d say about it, but I did expect something about using a strongly type language :D. I’m not sure if there’s a strongly type language for HTML5, maybe there’s a strongly type version of JavaScript?

Something I realized today is… I’m learning C++. I’m actually doing it. I told myself I would, a long time ago, but now I’m actually doing it. This is awesome.

Something else I realized… Unreal Engine uses C++. It’s always a good idea to know the programming language of an engine you want to use