How do you make a level?


(Simon) #21

This is a really good start for you @Johnnydb … the code is exactly what you want and should be easy to extend. (But then we expect nothing less from @Pharap).


(Josh Goebel) #22

I feel like someone has already written this code, or maybe it’s only on Gamebuino… all you need to do is have an array for buttons and increment each time it’s held during the frame loop… when it’s released you reset to 0 (or -1), etc… then if you want to do timing/repeat based things you just check if it’s held > amount or held % amount == 0, etc.

Pretty easy to write it yourself and call it write after you call pollButtons or whatever and then build out a few helper utility methods.

The reason the core lib doesn’t do this is it’d waste another 6 bytes of ram that most people probably don’t need - or else we’d probably already include the future.


(Pharap) #23

@Johnnydb, remember: anything you don’t understand or anything you need help with, just ask.


(JohnnydCoder) #24

I looked at the code and I think I understand it, but I do have a couple of questions.

First, you use this-> a lot in your code. Does this do anything special?

Second, in map.h you have two functions called getTile that have different int sizes, uint8_t and int16_t.

TileType getTile(uint8_t x, uint8_t y) const
	{
		// If coordinate is out of bounds
		if((x > this->width) || (y > this->height))
		{
			// Return 'void' tile
			return TileType::Void;
		}

		// Calculate array index
		const size_t index = this->getIndex(x, y);

		// Get pointer to tile data
		const TileType * tilePointer = &this->data[index];

		// Read tile data from progmem
		return readTileTypeFromProgmem(tilePointer);
	}

	TileType getTile(int16_t x, int16_t y) const
	{
		// If coordinate is out of bounds
		if((x < 0) || (y < 0) || (x > this->width) || (y > this->height))
		{
			// Return 'void' tile
			return TileType::Void;
		}

		// Calculate array index
		const size_t index = this->getIndex(x, y);

		// Get pointer to tile data
		const TileType * tilePointer = &this->data[index];

		// Read tile data from progmem
		return readTileTypeFromProgmem(tilePointer);
	}

Why do you have two?

Third, what is avr/pgmspace.h? What does it do?

Finally, why didn’t you use arduboy.collide() to track collision instead of the cross collision way? It could prevent the slippery corners. Was there a specific reason?

Thanks for taking the time to write this code! I appreciate it! :grinning:


(Pharap) #25

It’s to do with how classes/structs work.
(Classes and structs are basically the same thing in C++, there’s only one technical difference.)

Classes and structs can contain data (called ‘member variables’, or sometimes fields) and they can contain functions.

When you write a function in a class (called a ‘member function’) you can access the member variables through this->.

Technically it’s optional, but I always use it because I think it’s important to show that the variable being accessed is a ‘member variable’, as opposed to a ‘local variable’ or a ‘function parameter’.

(It’s worth noting that some people use ‘struct’ to mean “a class/struct that only has variables and no functions” and ‘class’ to mean “a class/struct that has variables and functions”, but that’s just convention. As I said earlier, as far as C++ is concerned, class and struct mean almost the same thing.)

Originally when I wrote it I had just the uint8_t version,
then when trying to fix some bugs I added the int16_t version.

I’m not sure if that actually contributed to fixing the bug,
but I left both in because I had everything working.

I can check later to see if removing the int16_t version and using just the uint8_t version causes any bugs, but I know for definite that removing the uint8_t version won’t cause any problems.

That’s the header that contains PROGMEM, memcpy_p and other things related to manipulating progmem.

The documentation for it is here:
https://www.nongnu.org/avr-libc/user-manual/group__avr__pgmspace.html

Because it’s not that simple.
Detecting collisions is one problem, but deciding how to react to collisions is another.

If you look at where the player is about to move to and discover that it’s going to collide with something, you then have to decide how to prevent that collision (a process called ‘collision resolution’), which is where the difficulty comes from.

You can’t just not move or your player might end up floating 2 pixels above the ground.
You could try to just put it on the ground but then it might not touch a wall that it’s supposed to, and that won’t help if it’s actually jumping rather than falling.

There’s lots of different ways to resolve collisions.
Some are more correct than others, some have different side effects (like slippery corners).

I wanted something that was quite simple to understand and quite quick to implement, and I discovered the ‘cross’/‘diamond’ technique in a reddit discussion somewhere.
(There wasn’t any code, but I could understand the idea from the explanation.)

Like I say in the comments, there are better ways that avoid the slippery corners, but I’d need more time to research and implement them, and they would probably be harder to understand.

No problem.
I promised I’d have a go and I like to keep my promises.

I don’t think I’ve ever actually written a platformer before so I had to do a bit of reading beforehand.
I didn’t know about the cross technique before so I’ve learnt something too (and if I ever see a platformer with slippery corners then I’ll know what’s going on).


(JohnnydCoder) #26

Sorry, one more question: how do functions loadMapFromProgmem() and readTileFromProgmem() work?


(Pharap) #27

No need to apologise, ask as many questions as necessary.

First I have to explain memcpy_p.
memcpy_p is a function that copies a whole block of bytes from progmem into RAM.

memcpy_p takes three arguments:

  • A pointer to a writable block of memory (RAM)
  • A pointer to a readable block of progmem
  • A size_t (a kind of integer) specifying how many bytes of memory to copy

In this case, the type of the memory being pointed to doesn’t matter,
the function ignores the actual types of the pointers and just copies the bytes directly.
Because of this it can be dangerous to use, so you have to be careful.

(Not dangerous in the sense that it’s going to damage the Arduboy,
but dangerous in the sense that it’s very easy to introduce bugs and errors.)

That’s one of the reasons I hid it behind a function with a better name.

I specifically chose to write readTileTypeFromProgmem to make it clear what was happening without having to expose the potentially confusing details.

Since you’re interested I’ll explain some of the detail.

  • Firstly readTileTypeFromProgmem is given a pointer to some progmem as an argument.
  • Then pgm_read_byte reads the byte at that progmem location.
  • Then static_cast<TileType> converts that byte of data into a TileType.
  • Then the result of that coversion is returned from the function.

Thus the function does exactly what it says on the tin: it reads a TileType from progmem.
(Technically it reads a byte of progmem first and then converts that into a TileType, but the result is the same.)

Part of the reason this is possible is because TileType is specified to be 1 byte in size by the : uint8_t part of its definition.
Its size can be specified manually because it’s a special kind of type called an enumeration.


Enumerations

Originally I wasn’t going to talk about enumerations unless you wanted to know more,
but then I decided they’re pretty important so I think you should know about them.

There are two kinds of enumerations:

  • ‘unscoped enumerations’, which are the more dangerous, more boring kind, specified by enum
  • ‘scoped enumerations’, which are much safer and much more useful, and are specified by enum class (or enum struct, but I’ve never actually seen someone use that)

I won’t talk much about unscoped enumerations (unless you really want to know about them) because they’re not very useful and you shouldn’t use them anyway.

The point of an enumeration type is to introduce a type that can store one of a limited group of values.
For example, TileType is a good example of an enumeration type because you have a limited number of tile types that you want to store.

Some other good examples of scoped enumerations are card suits (spade, heart, club, diamond), compass directions (north, east, south, west) and days of the week (monday, …, sunday).
All of these encompass a finite set of values.

One of the most common uses for them in Arduboy games are for representing game states (titlescreen, gameplay etc).

Underlying Type

Secretly though, all enumerations are actually integers underneath.
Every enumeration type has a thing called an ‘underlying type’,
which is the integer type used to implement the enumeration.

By default the underlying type is int, but the type can be chosen specifically.
On the Arduboy you’ll usually want to specify uint8_t or unsigned char, which is almost the same thing.
This is because int is 2 bytes and uint8_t is 1 byte.

Type Safety

But, scoped enumerations aren’t the same thing as integers because they have some extra ‘compiler magic’ applied to them.

The ‘magic’ is that it’s treated as a new type that’s not implicitly compatible with plain old integers,
which means it has a property called ‘type safety’.

Type safety is a very important programming concept becuase it’s very powerful.
It prevents errors by preventing the programmer from accidentally mixing different kinds of data.
(E.g. integers and tiles, integers and pointers.)

(Type safety is especially important in C++, and C++ provides many tools for maintaining type safety, especially since C++11.)

In the case of a scoped enumeration, it basically means you can do this:

// Correct: TileType::Grass is a kind of TileType
TileType tileType = TileType::Grass;

But you can’t do this:

// Error: an int is not a TileType
TileType tileType = 0;

You don’t get that power from unscoped enumerations (or from using integer types and #defines).
That’s why I say unscoped enumerations are boring and not very useful,
enumerations without type safety are like cake without icing or sandwiches without butter or fillings.

However, as I said earlier, a scoped enumeration is still an integer underneath,
and because C++ believes in giving the programer as much power as possible,
it’s possible to circumvent the type safety and treat TileType like an integer.

That’s why it’s possible to read a byte from progmem and treat it as an enumeration type.
As long as you use a static_cast you can convert any enumeration type to and from its underlying type.

The static_cast is like a kind of waiver or a kind of promise,
it’s like saying “I know what I’m doing and I accept the consequences of doing it”.

Thanks to that power you can do lots of other tricks with enumerations, but that’s a story for another day,
this is already getting longer than I intended.


Hopefully that all makes sense.
If you have any more questions, feel free to ask.


(JohnnydCoder) #28

Thanks @Pharap!

Two more questions: one, how do pointers work, and two, can I use your code in my game?


(Pharap) #29

I saw your comment earlier but didn’t have chance to reply until now.


Sure, but make sure you follow the licence rules.

Apache 2.0 only has two rules that you have to follow:

  • leave the licence notices intact
  • if you modify any of the code then “You must cause any modified files to carry prominent notices stating that You changed the files”
    • which basically just means you have to leave a comment saying // Modified by Johnnydb either near the top of the code or near the bit that you’ve modified

If you want you can even take the code I wrote and build your game on top of it rather than taking pieces of it and trying to integrate them into the code you already have.


Be warned, this is a pretty big topic so there’s going to be a lot of reading.

Pointers

Most kinds of computer memory are what is called “addressable”.
This means that every byte has a numeric address that can be used to access it.
E.g. if a chip had 1024 bytes of RAM you could specify the first byte (byte number 0) by specifying the address 0.

One easy way to think of addresses is to imagine that RAM is a big array of bytes and that an address is an index into that array.

In C++, those addresses are represented by pointers.
So essentially a pointer is actually another kind of integer with some compiler magic added on top.

But for pointers the ‘magic’ is even more important than it is for enums,
because reading or writing memory incorrectly can have serious side effects that could cause your program to crash.

E.g. if you write to the wrong area, overwrite something important or read the wrong bit of memory then your program could end up behaving oddly because it’s encountered a value that it didn’t expect or that it hasn’t been written to handle.

For that reason, it’s incredibly rare to treat a pointer as an integer,
and if done incorrectly it could have some bad side effects.

In general, raw pointers are considered ‘dangerous’,
so they should normally be avoided in favour of other approaches.

However on an embedded system you’re forced to do at least some low level programming,
so you’re more likely to need to use pointers than you would be if programming for a desktop/laptop.

Progmem and EEPROM Pointers

AVR chips behave quite differently to other chips when it comes to memory.
Most modern chips (and even some old ones) have just one address space that covers multiple kind of memory.
But AVR chips have three different address spaces, one for each different type of memory:

  • RAM
  • Flash (progmem)
  • EEPROM

As a result of this, the CPU has different instructions for accessing each address space.
That’s why on the Arduboy you have to use functions/macros like pgm_read_byte and eeprom_read_byte/EEPROM.read.

The pgm_read_xxx, eeprom_read_xxx and eeprom_write_xxx functions/macros specifically require a pointer to work,
but the EEPROM functions can work with just numbers (because as I said earlier, pointers are just integers underneath).

Using Pointers

The reason pointers are better than using integers for accessing RAM is the same reason scoped enums are better than integers: type safety.

Here’s an example of some basic use of a pointer:

int i = 5;

// ip is a pointer to the address of i
int * ip = &i;

i = 8;

// Get the value stored in the variable that ip is pointing to
int a= *ip;

// This prints '8'
arduboy.print(a);

char c = 2;

// Error, the type of &c is char *, not int *
int * cp = &c;

(You’re unlikely to see pointers used like this in real code, this is just an example to show the syntax.)

Adding a * onto the end of any type name makes it a pointer to that type.

E.g.

  • int * is a pointer to an int
  • char * is a pointer to a char
  • int * * is a pointer to a pointer to an int

That means that the syntax for declaring a pointer is _type_ * _identifier_.

The syntax for getting the address of (i.e. getting a pointer to) a variable/object is &_identifier_

(& in this context is the ‘address-of operator’. Not to be confused with the ‘bitwise and’ operator, which uses the same symbol but operates on two values/objects.)

The syntax for dereferencing a pointer (i.e. getting the value that a pointer points to) is *_identifier_.

(* in this context is the ‘indirection operator’. Not to be confused with the ‘multiplication’ operator, which uses the same symbol but operates on two values/objects/)

Null Pointer

There’s a speciall kind of pointer called the ‘null pointer’ which represents a pointer that doesn’t point to anything.

In C, C++98 and C++03, this is represented by the NULL macro.
But in C++11 they introduced nullptr, which is much better because it’s a proper keyword and it has additional safety features.
The Arduino IDE supports C++11, so nullptr is what you should stick to using.

References

I won’t go into detail about these in this comment because it’ll be another page of explanation.
(If you want to know more about them, ask and I’ll explain them in a separate comment.)

Basically these behave a bit like pointers in that they refer to an object,
but they have some important extra rules:

  • A reference can only be assigned once (i.e. they can only ever refer to one object)
  • A reference cannot be null
    • Technically it’s possible on some compilers, but the rules of the standard forbid it, and it’s not something you can do accidentally, you’d have to be trying to do it on purpose

Because of this, references are much safer and thus they are greatly preferred over pointers.


(JohnnydCoder) #30

Three more questions @Pharap:

First, my player is 16 by 24, so he looks like he has sunk into the ground, so how do I make him look as if he was standing on the ground?

Second, how can I make an enemy walk on the ground and make contact with the world? I sort of want a Mario style type of enemy (like a Goomba if you know what that is).

Finally, do I have to say Modified by Johnnydb if I build on your code?

Thanks for your explanation of pointers! I never knew how to use them or what they do, but now I do!


(Pharap) #31

Hrm, that’s going to complicate things a bit.
Wish I’d thought of that before writing the code instead of making assumptions.

I know half of what needs to be done but I need time to figure out which variables to substitute.

Is the whole of the player still expected to collide with the world?
If so a 16x24 player isn’t going to fit through 1 tile high vertical gaps, it’ll need at least 2 tiles.
Also if the player stands on a high enough tile then the player’s head will be offscreen,
in which case I might need to stop the player colliding with the ceiling.

Alternatively, you could have the player physics work as if the player were 16x16,
but still draw the sprite as 16x24, which would be a lot easier for calculations and movement,
but it might look a bit odd.

It’ll have to use the same collision code as the player does.

Would the enemy be the same size or a different size?

You do, but that’s a good thing - you want to put your mark on it to show that you’ve modified it and thus some of the code is your own code.
You can even put a second copyright notice on it if you want, e.g.

// 
// Modifications Copyright (C) 2019 Johnnydb (@JohnnydCoder)
// 

No problem.

Normally in C++ you don’t deal with raw pointers,
they’re either hidden behind smart pointers or you use references instead,
but on the Arduboy you have to encounter pointers for reading from progmem at least.


(Pharap) #32

Here’s an example of drawing the player as 16x24, but keeping the collision mechanics the same:


I’m half tempted to suggest joining up with you to work on the game together,
but I’m not sure if I have time to work on it properly.


(JohnnydCoder) #33

Thanks for the code!

I should have mentioned it instead of having you guess. Sorry. :slightly_smiling_face:

It will be a 16 by 16 size.


(Pharap) #34

Then you’ll be able to use the same collision code for moving enemies,
but it will need a small bit of adapting to be able to work with any entity.


(JohnnydCoder) #35

I created my own enemy collision function but my enemy is moving through the tiles that are solid.

Can you see if I did anything wrong in this code?

if (arduboy.everyXFrames(3))
    {
      if (enemy.isMovingL) 
      {
        enemy.x -= movementSpeed;
      }
      if (!enemy.isMovingL)
      {
        enemy.x += movementSpeed;
      }
      if (enemy.isFalling)
      {
       enemy.y -= gravitySpeed;
      }
   }
   
   this->updateEnemyPosition();
void updateEnemyPosition()
  {
    // Figure out the point that the enemy should be moving to
    int16_t newEnemyX = (this->enemy.x + this->movementSpeed);
    int16_t newEnemyY = (this->enemy.y + this->movementSpeed);
    
    // Figure out the tile coordinate that the player should be moving to
    const int16_t tileX = (newEnemyX / tileWidth);
    const int16_t tileY = (newEnemyY / tileHeight);

     // Find the x coordinate of the enemy's new right side
    const int16_t rightX = (newEnemyX + halfTileWidth);

    // Find which tile the enemy's new right side is in
    const int16_t rightTileX = (rightX / tileWidth);

    // Find the tile the enemy is trying to move into
    const TileType rightTile = map.getTile(rightTileX, tileY);

    // If the tile is solid
    if(isSolid(rightTile))
    {
      // Adjust the enemy's position to prevent collision
      newEnemyX = ((rightTileX * tileWidth) - halfTileWidth);
    }
    
    // Find the x coordinate of the player's new left side
    const int16_t leftX = ((newEnemyX - halfTileWidth) - 1);
    
    // Find which tile the player's new left side is in
    const int16_t leftTileX = (leftX / tileWidth);

    // Find the tile the player is trying to move into
    const TileType leftTile = map.getTile(leftTileX, tileY);

    // If the tile is solid
    if(isSolid(leftTile))
    {
      // Adjust the player's position to prevent collision
      newEnemyX = (((leftTileX + 1) * tileWidth) + halfTileWidth);
    }
  }

I sort of copied your code and adapted it to an enemy type of AI.

Thanks!


(Pharap) #36

Yeah, a few problems.

Firstly the collision checking result is being ignored.
(You calculate newEnemyX and newEnemyY, then do nothing with them.)

Secondly you’re adding ‘movementSpeed’ to both x and y,
which means you’ll move the enemy diagonally all the time:

What you need to be doing is giving enemy an xVelocity and a yVelocity,
setting xVelocity to what is currently movementSpeed and setting yVelocity to gravitySpeed.

You could reuse the Entity class provided in TileWorld to represent enemies.
Then you could change void updatePlayerPosition() to void updateEntityPosition(Entity & entity) and use that to handle collisions for all entities.

Like so:

If you’re wondering what & does, that make entity a reference.
It allows entity to be modified by the function.
Without it, entity would be copied and only the copy would be modified.

After making that change:

  • enemy.xVelocity = movementSpeed; makes the enemy move right
  • enemy.xVelocity = -movementSpeed; makes the enemy move left
  • enemy.yVelocity = gravitySpeed; applies gravity to the enemy

You just have to remember to do this->updateEntityPosition(enemy) to make the enemy actually move and do collision checking.


(JohnnydCoder) #37

I did what you told me to do about the function updateEntityPosition() but the opponent still isn’t responding to the world around it. Am I missing something?

Here’s the updated game.h code:

#pragma once

//
// Copyright (C) 2019 Pharap (@Pharap)
// 
// Modifications Copyright (C) 2019 Johnnydb (@JohnnydCoder)
// 
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#include <Arduboy2.h>
#include <stdint.h>
#include <avr/pgmspace.h>

#include "TileImages.h"
#include "TileType.h"
#include "WorldData.h"
#include "Entity.h"
#include "Camera.h"

const unsigned char panda[] PROGMEM =
{
  0x06, 0x09, 0x09, 0xf6, 0x01, 0x01, 0x01, 0x01, 0x1d, 0x3d, 0x35, 0x3d, 0x01, 0xfe, 0x48, 0x30,
0x00, 0x00, 0xfe, 0xff, 0xff, 0xff, 0xe1, 0xed, 0xdb, 0xd3, 0xd7, 0xd7, 0xef, 0xff, 0xfe, 0x00,
0x00, 0x00, 0x00, 0x7f, 0x83, 0x83, 0x9f, 0xa1, 0xc1, 0x01, 0xff, 0x83, 0x83, 0xbf, 0xa0, 0xc0,

};

class Game
{
private:
  static constexpr int16_t movementSpeed = 1;
  static constexpr int16_t gravitySpeed = 1;

  static constexpr int16_t centreScreenX = (WIDTH / 2);
  static constexpr int16_t centreScreenY = (HEIGHT / 2);

  static constexpr int16_t halfTileWidth = (tileWidth / 2);
  static constexpr int16_t halfTileHeight = (tileHeight / 2);
  
  static constexpr int16_t playerWidth = 16;
  static constexpr int16_t playerHeight = 24;

  static constexpr int16_t halfPlayerWidth = (playerWidth / 2);
  static constexpr int16_t halfPlayerHeight = (playerHeight / 2);

private:
  Arduboy2 arduboy;
  Map map;
  Camera camera;
  Entity playerEntity { centreScreenX, 0, 0, 0 };
  Entity enemy { centreScreenX + playerWidth, 32, 0, 0};

public:
  void setup()
  {
    this->arduboy.begin();

    // Load map
    this->loadMapFromProgmem(map0);
  }

  void loop()
  {
    if(!this->arduboy.nextFrame())
      return;

    this->arduboy.pollButtons();

    this->arduboy.clear();

    // Update and render
    this->updateGameplay();
    this->renderGameplay();

    this->arduboy.display();
  }

  void updateGameplay()
  {
    // Handle xVelocity
    int16_t xVelocity = 0;

    if(this->arduboy.pressed(LEFT_BUTTON))
    {
      xVelocity -= movementSpeed;
    }

    if(this->arduboy.pressed(RIGHT_BUTTON))
    {
      xVelocity += movementSpeed;
    }

    this->playerEntity.xVelocity = xVelocity;

    // Handle jumping (yVelocity)
    if(this->arduboy.justPressed(A_BUTTON))
    {
      if(this->playerEntity.yVelocity > 0)
        this->playerEntity.yVelocity = -9;
    }

    // If player is jumping
    if(this->playerEntity.yVelocity < 0)
    {
      // Slowly reduce negative velocity with gravity
      this->playerEntity.yVelocity += gravitySpeed;
    }
    else
    {
      // Else apply normal gravity
      this->playerEntity.yVelocity = gravitySpeed;
    }

    // Update the player's position
    this->updateEntityPosition(this->playerEntity);

    // Figure out the new map position based on the player's current position
    const int16_t newMapX = (this->playerEntity.x - centreScreenX);
    const int16_t newMapY = (this->playerEntity.y - centreScreenY);

    this->camera.x = ((newMapX < 0) ? 0 : newMapX);
    this->camera.y = ((newMapY < 0) ? 0 : newMapY);

if (enemy.x < playerEntity.x)
    {
      enemy.xVelocity = movementSpeed;
    }
     if (enemy.x > playerEntity.x)
    {
      enemy.xVelocity = -movementSpeed;
    }

    this->updateEntityPosition(this->enemy);
 };
 
  void drawEntities()
  {
    constexpr int16_t playerDrawOffsetX = (halfTileWidth + (playerWidth - tileWidth));
    constexpr int16_t playerDrawOffsetY = (halfTileHeight + (playerHeight - tileHeight));
  
    const int16_t x = ((this->playerEntity.x - playerDrawOffsetX) - this->camera.x);
    const int16_t y = ((this->playerEntity.y - playerDrawOffsetY) - this->camera.y);

    this->arduboy.fillRect(x, y, playerWidth, playerHeight, WHITE);
    this->arduboy.drawBitmap(x, y, panda, playerWidth, playerHeight, BLACK);

    arduboy.fillRect(enemy.x, enemy.y, 16, 16, BLACK);
  }

  void updateEntityPosition(Entity & entity)
  {
    // Figure out the point that the player should be moving to
   int16_t newX = (entity.x + entity.xVelocity);
   int16_t newY = (entity.y + entity.yVelocity);

    // Figure out the tile coordinate that the player should be moving to
    const int16_t tileX = (newX / tileWidth);
    const int16_t tileY = (newY / tileHeight);

    // Find the x coordinate of the player's new right side
    const int16_t rightX = (newX + halfTileWidth);

    // Find which tile the player's new right side is in
    const int16_t rightTileX = (rightX / tileWidth);

    // Find the tile the player is trying to move into
    const TileType rightTile = map.getTile(rightTileX, tileY);

    // If the tile is solid
    if(isSolid(rightTile))
    {
      // Adjust the player's position to prevent collision
      newX = ((rightTileX * tileWidth) - halfTileWidth);
    }
    
    // Find the x coordinate of the player's new left side
    const int16_t leftX = ((newX - halfTileWidth) - 1);
    
    // Find which tile the player's new left side is in
    const int16_t leftTileX = (leftX / tileWidth);

    // Find the tile the player is trying to move into
    const TileType leftTile = map.getTile(leftTileX, tileY);

    // If the tile is solid
    if(isSolid(leftTile))
    {
      // Adjust the player's position to prevent collision
      newX = (((leftTileX + 1) * tileWidth) + halfTileWidth);
    }

    // Find the x coordinate of the player's new bottom side
    const int16_t bottomY = (newY + halfTileHeight);

    // Find which tile the player's new bottom side is in
    const int16_t bottomTileY = (bottomY / tileHeight);

    // Find the tile the player is trying to move into
    const TileType bottomTile = map.getTile(tileX, bottomTileY);

    if(isSolid(bottomTile))
    {
      // Adjust the player's position to prevent collision
      newY = ((bottomTileY * tileHeight) - halfTileHeight);
    }

    // Find the x coordinate of the player's new top side
    const int16_t topY = ((newY - halfTileHeight) - 1);

    // Find which tile the player's new top side is in
    const int16_t topTileY = (topY / tileHeight);

    // Find the tile the player is trying to move into
    const TileType topTile = map.getTile(tileX, topTileY);

    // If the tile is solid
    if(isSolid(topTile))
    {
      // Adjust the player's position to prevent collision
      newY = (((topTileY + 1) * tileHeight) + halfTileHeight);
    }

    // Assign the player's new position
    // Whilst preventing the position from going out of bounds
    entity.x = ((newX > halfTileHeight) ? newX : halfTileWidth);
    entity.y = ((newY > halfTileHeight) ? newY : halfTileHeight);
  }
  void renderGameplay()
  {
    // Draw map
    this->drawMap(this->camera.x, this->camera.y, this->map);

    // Draw player
    this->drawEntities();

    // Print camera position
    this->arduboy.print(this->camera.x);
    this->arduboy.print(F(", "));
    this->arduboy.println(this->camera.y);

    // Printer player entity position
    this->arduboy.print(this->playerEntity.x);
    this->arduboy.print(F(", "));
    this->arduboy.println(this->playerEntity.y);
  }

  void loadMapFromProgmem(const Map & progmemMap)
  {
    // Copy map from progmem into map
    memcpy_P(&this->map, &progmemMap, sizeof(Map));
  }

  void drawMap(int16_t mapX, int16_t mapY, Map map)
  {
    constexpr size_t screenTileWidth = ((WIDTH / tileWidth) + 1);
    constexpr size_t screenTileHeight = ((HEIGHT / tileHeight) + 1);

    for(uint8_t y = 0; y < screenTileHeight; ++y)
    {
      const int16_t mapTileY = (mapY / tileHeight);
      const int16_t tileY = (mapTileY + y);
  
      // If tile is out of bounds, continue next iteration
      if(tileY < 0 || tileY >= map.getHeight())
        continue;
  
      const int16_t mapRemainderY = (mapY % tileHeight);
      const int16_t drawY = ((y * tileHeight) - mapRemainderY);
  
      for(uint8_t x = 0; x < screenTileWidth; ++x)
      {
        const int16_t mapTileX = (mapX / tileWidth);
        const int16_t tileX = (mapTileX + x);
  
        // If tile is out of bounds, continue next iteration
        if(tileX < 0 || tileX >= map.getWidth())
          continue;
  
        const int16_t mapRemainderX = (mapX % tileWidth); 
        const int16_t drawX = ((x * tileWidth) - mapRemainderX);

        // Get tile type from map
        TileType tileType = map.getTile(tileX, tileY);

        // Draw tile
        Sprites::drawOverwrite(drawX, drawY, tileImages, getTileIndex(tileType));
      }
    }
  }
};

One more question: is there any way to place enemies specifically on one part of a map?


(Pharap) #38

There’s several problems:

  • The enemy isn’t being drawn in the correct place.
    • The x and y used by entities is actually the centre of the entity’s collision box, not the top left
  • The enemy isn’t being affected by gravity, so it just floats.
  • The enemy starts too close to the player, so it just snaps straight to the player and hangs there.

And with panda graphics:

When you create the enemy you’re already providing it with an x and a y position.


Of course, the enemy doesn’t currently jump.
Decent AI is very hard to program.

Some suggestions for what to do next:

You might want to lower the enemy’s speed if it’s going to home in on the player like that, otherwise it’s going to be almost impossible to avoid (unless you give the player a ‘run’ button or something).

You might want to stop the enemies chasing after the player if they go offscreen or if they’re far enough away from the player.

Find out how masks work so you can switch to drawing the panda with Sprites::drawPlusMask or Sprites::drawExternalMask.


I think you can already see that platformers are a lot more complicated than they get given credit for.


(JohnnydCoder) #39

I meant to say is there a way to make generate enemies anywhere (like make multiple enemies appear on a course).

Also, is there any way I can make the enemy turn around when it walks into a wall?

Here is a drawing of what I mean if that helps:

| <-O then | O->


(Pharap) #40

Yes, you’d need an array of enemies.

Yes, I know what you mean.
In technical terms you want to “invert their horizontal velocity upon collision with a solid tile”.

But it will either mean giving enemies different collision code or adding some other new functions.

I’ll have to demonstrate tomorrow, I don’t have time at the moment because it’s late here.


If you want to, while I’m away have a read of chapters 6.1-6.3 of the ‘learncpp’ tutorial and see if you can figure out the enemy thing on your own.

Don’t worry if you can’t, I’ll show you when I have chance,
but if you can figure at least some of it out then that’ll be a bit of extra progress.