Help me with C++ for my game


#83

Ok, this is a good moment to learn more about pointers I think?

The way I see it I have two options. I can either keep each segment of a tentacle seperate, and have a “parent” and “child” pointer or reference, but I don’t like this method as I can’t iterate through all the segments.

The second option is more or less what I’m doing here:

#pragma once

    class TentacleSegment {
    public:
        float x = 0;
        float y = 0;
        float length = 5;
        float width = 3;
    public:
        TentacleSegment(float len, float w = 3) {
            length = len;
            width = w;
        }
};

class Tentacle {
    private:
        float x;
        float y;
        uint8_t segmentCount = 5;
        float length = 10;
        float segmentLength = (length / segmentCount);
        TentacleSegment segments[segmentCount](segmentLength);
    public:
        Tentacle(float x1, float y1, uint8_t count = 3) {
            x = x1;
            y = y1;
            segmentCount = count;
        }
        void update();
};

Tentacle::update() {
    for(uint8_t i = 0; i < segmentCount; i++) {
        
    }
}

I want to be able to have tentacles with a different number of segments. One way is to have a fixed array and ignore any segments that aren’t supposed to exists. What I’d like to do is pass in the number of segments that will be needed in the constructor, but if I understand properly, classes need to have a fixed size meaning I can’t do this?


(Pharap) #84

That’s my job. :P
(Kind of. I don’t get paid for it.)

I thought Gamemaker already did that? (In 2D at least.)

Ah yes. That one you have to be careful with, sometimes that slips through cracks.

I find that sometimes people prefer to do raycasting or line intersections for that reason.

Not all engines cover all the features that you’d want, but I wouldn’t be surprised if Gamemaker is missing some.

In fact, one of the places where Gamemaker goes wrong is that it doesn’t really know what it wants to be.

When it comes to game engines, they often perform better if they’re either tailored to a certain game genre (e.g. RTS, RPG, Point&Click) or just provide the bare minimum and let people build on top of them.
(Love2D is a good example of the latter, it’s mainly just a friendly wrapper for SDL that can be built upon.)

You could argue that gamemaker tries to be the ‘bare minimum’, but it has a lot of features like ‘gridsnap’ and requiring objects to have coordinates that make it clear it was originally intended for a certain kind of 2D game and the 3D features were bolted on top as an afterthought.

If you’re thinking that will save memory, then that’s not necessarily the case.
It depends on what’s smaller: the tentacle graphic or the code for the drawTriangle function.
If a tentacle sprite is smaller, then using the sprite would possibly be cheaper than just using the drawTriangle function once.

It’s always better to try both than to assume either way.

That’s why I wrap all my emoticons in graves (which is supposed to be for inline code).
It stops the comment processor from tranlating them into emoji.

(Fun fact: there’s actually a difference. Emoticons refer to a group of normal punctuation and letters intented to be interpreted as a face or image. Emoji refers to a kind of glyph or image specially designed to actually represent a face or image.)

20 years ago was the year it was first standardised (C++98).
A lot has happened since then. Most importantly C++11.

If you haven’t seen me mention it yet, there’s a good tutorial here and a good reference here (though the English version is more complete).
But you must remember that the Arduino environment doesn’t have access to the C++ standard library, it uses the C99 standard library instead.

The easiest way to understand pointers:
Imagine that RAM is just a massive byte array.
A pointer is an index into that array.

(That’s glossing over a few details like memory alignment, but that’s the core idea.)

Classes do have to have a fixed size.

There’s a way to have variable length structures, but it’s a bad idea on a device that only has ~2.5KB of RAM (of which at least 1KB is dedicated to the frame buffer).

This is usually the best option on the Arduboy, so if in doubt, go for this.

Before I can give a good answer about this, explain to me:

  • What exactly is a tentacle segment supposed to be?
  • What exactly is a tentacle is supposed to be?
  • Is it like the snake in snake?
  • Is it supposed to be grid aligned or free flowing?
  • Are the sizes constants?

At the heart of this will probably be a data structure.
What that data structure should be will be defined by the behaviour needed.

This won’t work.

  • The size of an array must be a constant expression (i.e. a literal value or a constexpr variable)
  • That won’t call the constructor for every element, without a certain C++17 feature you have to call the constructors manually for every object (though I don’t think that’s what you want/need to do in this case).

#85

Not collision with tiles no. There’s place_meeting(x, y, passNameOfObject) but there’s no is_tile_solid(x, y), I made all of those for my project. They’re planning on introducing it later in GameMaker Studio 2 apparently

As I said before however, GameMaker is much better than it used to be (especially GameMaker Studio 2 but Studio 1.4 as well)

I want to use different widths and heights for each segment and I need the ability to rotate them, which is done much more easily with plain triangles

That’s a good idea :D

Ok, will do

Interesting, thank you!

Information like this is great for pedantic people like me

The result should be my video that I linked a bit above, but in 128x64 resolution.

More specifically: the tentacles follow a certain target (could be the player position) this part is simple.

Then, the tentacle tries to reach it… this way:

  1. Take the first segment. Make it point towards the target. Move the segment to the target.
  2. Repeat with the next segment.
  3. If the last segment no longer is in the same position, get the difference in x and y between it and it’s initial position, and move all the segments back.

I’m not sure if this is clear?

Something like this:

int lengthdir_x(int, int); //returns the distance value of x based on the direction and the distance you'd want to move
int lenghtdir_y(int, int); // same as the other but for y

segment[i].angle = get_angle_to(segment[i].x, segment[i].y, playerx, playery);
for(var i = 1; i < segmentCount; i++) {
    var targetx = segment[i-1].x;
    var targety = segment[i-1].y;
    segment[i].angle = get_angle_to(segment[i].x, segment[i].y, targetx, targety);
    segment[i].x = targetx - lengthdir_x(segment[i].width, segment[i].angle);
    segment[i].y = targety - lengthdir_y(segment[i].width, segment[i].angle);
}
var distance_x = segment[lastSegment].x - segment[lastSegment].startx;
var distance_y = segment[lastSegment].y - segment[lastSegment].starty;
for(var i = 0; i < segmentCount; i++) {
    segment[i].x += distance_x;
    segment[i].y += distance_y;
}

Okay, that’s obviously not c++ but you should get what I’m trying to do. That’s inverse kinematics which… is used in robotics so I might actually use that with a real robot later, that’d be great :D

Anyway if my explaination isn’t clear enough, here’s the video series I learned this from: https://youtu.be/7t54saw9I8k


(Pharap) #86

Hrm, probably because the tiles are just imaginary and aren’t actually stored anywhere as such.

They’d have to change a heck of a lot to convince me.
(There are some really big problems.)

(Their “Evaluation Order” documentation leaks the fact they actually convert the GML to C++ first with a minimal amount of processing.)

That’s going to start to eat processing power. Rotation isn’t particularly cheap.

If this is going to be anything more than tentacles grabbing at a box you might have to drop the floats.
We’ll see how it goes.

This is going to be the hard part.
You’re going to need to keep track of the previous positions to be able to move them back.

I think it’s best to have a hard limit on the number of segments a tentacle can have.
(Maybe 8 or 10? 10-tacles. :P)

It’s suddently dawned on me why you’ve been using ‘snake_case’ for function names.

You’d need a more than just this.
Robotic arms aren’t just made of several segments, each segment has a servo motor,
so you have to figure out what angle to set each servo to,
and each angle is relative to its ‘parent’ segment.

(Typically that sort of thing is handled with matrices. Matrices are a complex, boring and frankly hellish mathematical construct, but if you can wrap your head around them, they’re one of the key building blocks to 3D modelling so they come in really handy.)

The only things you haven’t made clear are whether the length and width of a tentacle segment change, and how you determine the three points of the triangle.


Another tip:
If you want to infer the type of an expression in C++, use auto.

// Inferred as 'int'
auto value = 1;
// Inferred as 'Entity'
auto entity = Entity();
// Inferred as 'EntityType'
auto type = EntityType::None;

(But it only works with globals and locals, not member functions. (I think.))


#87

No, that’s extremely easy. You just keep track of the original position of one segment, and move everything by how much you need to move that one. Only one calculation (technically two, one for the x axis and one for the y axis)

Just a for loop, and move back everything by the same amount. This means the tentacle doesn’t touch whatever it was supposed to touch, but it’s fine because it self corrects after a few frames. That’s what makes it look alive

No need for rotation if you can draw triangles - a rectangle at any angle is simply two triangles

I’m gonna need the lenghtdir functions I talked about, which means I’ll have to make them. It has to do with sine and cosine, not sure if that’ll be costly on the Arduboy. Still it’s a fun thing to try

I might used fixed points instead of floats by the way (or maybe it would even work with just integers, I’ll have to see)

Mostly habbit. I haven’t learned the usual C++ conventions yet, but I noticed classes usualy start with a Capital letter, and everything is on camelCase (I think it’s called camelCase?)

I’ve been working on sprites, I’m learning by imitating so the resemblance to Arduventure isn’t an accident. The biggest difference is that I’m using 8x8 sprites instead of 16x16.


(Pharap) #88

What do you mean by ‘move back’.
I was taking that to mean ‘revert the segments to their previous positions’.

But you must rotate the triangles, or have some means of determining the points that make up the triangles.

I already implemented them while I was waiting for you to reply.
(I’ll get to that in a moment.)

I’ll get to this in a moment, but in a word: yes.

That would probably be cheaper, but there’s a snag with using fixed points.
I don’t know how to implement atan2, sqrt or acos with fixed points, and one or two of those is needed to implement get_angle_to.
(I’ll explain at the end.)

The typical names are ‘PascalCase’, ‘camelCase’, ‘snake_case’, ‘MACRO_CASE’.

In the C++ stdlib, everything is snake case or macro case (except maybe arguments, I think those are camelCase).

On the Arduboy and Arduino, type names are pascal case, macros are macro case and everything else is camel case.

C++ as a whole doesn’t have a single convention, people use different arrangements in different programs and libraries.
The only golden rule is that macros are always in macro case.

You’re free to use either, as long as everything’s monochrome.


I’ve got a block of code that’s roughly what you’ve written and some additional structure that would be necessary to implement it, but I’ve got to be honest these are some pretty expensive instructions for an AVR chip.

Af far as I’m aware, there are two ways of implementing get_angle_to.
The first involves two sqrt calls and an acos.
The other involves two atan2 calls (and you have to be careful to avoid zero-length vectors when using it).

Given that you’d be calling that once for every segment on the screen, that’s really going to chew up processing power.
Not to mention lengthDir needs to call both cos and sin.
(Fun fact: lengthDir is actually just converting from polar coordinates to cartesian coordinates.)

So, if you’ll prepare your head for information overload, here’s my ~95% accurate implementation according to the code you’ve showed me.

Big blob of code
#pragma once

struct Vector2
{
	float x;
	float y;
	
	Vector2() = default;
	constexpr Vector2(float x, float y)
		: x(x), y(y) {}
		
	constexpr float getMagnitudeSquared() const
	{
		return ((this->x * this->x) + (this->y * this->y));
	}
	
	float getMagnitude() const
	{
		return sqrt(this->getMagnitudeSquared());
	}
};

struct Point2
{
	float x;
	float y;
	
	Point2() = default;
	constexpr Point2(float x, float y)
		: x(x), y(y) {}
		
	constexpr Vector2 toVector() const
	{
		return Vector2(this->x, this->y);
	}
};

inline constexpr Point2 operator+(Point2 point, Vector2 vector)
{
	return Point2(point.x + vector.x, point.y + vector.y);
}

inline constexpr Point2 operator-(Point2 point, Vector2 vector)
{
	return Point2(point.x - vector.x, point.y - vector.y);
}

inline constexpr Vector2 operator*(Vector2 vector, float scale)
{
	return Vector2(vector.x * scale, vector.y * sacle);
}

Vector2 getUnitVector(float angle)
{
	return Vector2(cos(angle), sin(angle));
}

Vector2 getVectorFromPolarCoordinate(float angle, float length)
{
	return (getUnitVector(angle) * length);
}

float constexpr dotProduct(Vector2 left, Vector2 right)
{
	// Equivalent to (left.getMagnitude() * right.getMagnitude()) * cos(angle)
	return ((left.x * right.x) + (left.y * right.y));
}

float angleBetween(Vector2 left, Vector2 right)
{
	return acos(dotProduct(left, right) / (left.getMagnitude() * right.getMagnitude()));
}

float angleBetween(Point2 left, Point2 right)
{
	return angleBetween(left.toVector(), right.toVector());
}

class TentacleSegment
{
public:
	Point2 position;
	Point2 startPosition;
	float length;
	float width;
	float angle;
	
public:
	TentacleSegment() = default;
	TentacleSegment(float x, float y, float length, float width)
		: position(x, y), startPosition(x,y), length(length), width(width), angle(0) {}
};


class Tentacle
{
private:
	constexpr size_t SegmentCapacity = 10;
	constexpr float DefaultLength = 10.0f;
	
public:
	Point2 position;
	float length;
	float segmentLength;
	uint8_t segmentCount;
	TentacleSegment segments[SegmentCapacity];
	
public:
	Tentacle(float x, float y, uint8_t segmentCount)
		: position(x, y), length(DefaultLength), segmentLength(DefaultLength / segmentCount), segmentCount(segmentCount) {}
};


class Player
{
public:
	Point2 position;
};

class World
{
private:
	constexpr size_t TentacleCapacity = 10;
	
private:
	Player player;
	uint8_t tentacleCount;
	Tentacle tentacles[TentacleCapacity];
	
public:
	
	
private:
	void updateTentacles();
	void updateTentacle(Tentacle & tentacle);
};

void World::updateTentacles()
{
	
}

void World::updateTentacle(Tentacle & tentacle)
{
	tentacle.segments[0] = angleBetween(tentacle.segments[0].position, player.position);
	for(uint8_t i = 1; i < tentacle.segmentCount; ++i)
	{
		auto target = tentacle.segments[i - 1].position;
		auto & segment = tentacle.segments[i];
		segment.angle = angleBetween(segment.position, target);
		segment.position = (target - getVectorFromPolarCoordinate(segment.angle, segment.width));
	}
	auto lastIndex = (tentacleCount - 1);
	auto distance = tentacle.segments[lastIndex].position - tentacle.segments[lastIndex].startPosition;
	for(uint8_t i = 0; i < tentacle.segmentCount; ++i)
	{
		tentacle.segments[i].position += distance;
	}	
}

Also for the record:

  • A TentacleSegment is 28 bytes in size
  • A Tentacle is 297 bytes in size
  • A World is 2979 bytes in size

There are 2560 bytes of RAM in total.
The frame buffer uses 1024 of that, leaving 1536 bytes, of which some is used by Arduboy2 state variables and some is needed for the stack.

Are you happy with just the 2 tentacles?
Or some really short tentacles? :P


#89

I love how you kept that fun answer for the end :D
(I am now addicted to the “:D” symbol)

Thank you for educating me!

I was referring to it in the sense of “not straight ripping of sprites from another game”. I don’t like copying but it’s a good way to learn

Nice link!

Hahaha this is amazing. Short tentacles might do, it was mostly as an experiment
Thanks for the code, I’ll be able to read through it and probably learn some things


#90

If you have a second and see this, how can I turn this:

1,1,0,0,0,2,0,0,0,0,0,12,13,1,1,1,
0,5,5,5,0,2,1,12,13,4,4,4,15,0,11,1,
0,5,5,5,5,2,1,14,15,4,4,4,1,0,11,11,
1,6,5,5,5,2,1,1,0,6,7,6,1,1,1,0,
1,1,6,7,6,2,2,2,2,2,2,2,2,2,2,2,
2,2,2,2,2,2,1,1,1,11,0,1,1,1,0,0,
1,1,1,1,1,1,0,11,11,11,11,0,0,0,0,1,
1,1,1,0,0,0,0,0,11,11,1,1,1,1,1,1,

Into a uint8_t 2d array?

Also, this is read from left to right (x axis). So the line at the top are all at y=0

Edit: I think I got it, but I’ll have to switch my tile functions to invert the x and y, which should be easy

uint8_t level[16][8] = {
   {1,1,0,0,0,2,0,0,0,0,0,12,13,1,1,1},
   {0,5,5,5,0,2,1,12,13,4,4,4,15,0,11,1},
   {0,5,5,5,5,2,1,14,15,4,4,4,1,0,11,11}, //etc
};

(Pharap) #91

This is why it’s important to ask questions.
You won’t find that kind of knowledge in a text book, it’s something you gain through experience (or asking someone who has that experience).

It depends what you pick. Some art assets are free for use.
Arduventure’s art assets aren’t unfortunately.
(Personally I respect their decision not to licence the assets.)
But other games sometimes have their art licensed.

For example Minesweeper’s art is all BY-NC-SA 4.0.

mathsisfun is the best maths website.
I absolutely hate maths (moreso the mathematicians who overcomplicate everything - mathsisfun proves it can be explained simply without all the complex pseudo-Latin tripe) so that website is a godsend.

7 years ago I didn’t know the difference between RAM and a hard drive and I’d never seen a line of code in my life, now I’m doing wizardry.

It’s completely possible if you’re determined.

(Determination in the face of extreme verbosity and boringness is the ultimate programming skill. :P)

(Also, remember it may look like magic, but I’ve written all these functions before, many times over.)

I’m happy to answer any questions whatsoever.
(I purposely didn’t try to dumb it down, so I’m expecting at least 3-4 things you’ve never seen before.)

sizeof(Tentacle) = 8 + 4 + 4 + 1 + (SegmentCapacity * 28)
sizeof(World) = 8 + 1 + (TentacleCapacity * sizeof(Tentacle))

If you had 10 lots of 6 segment tentacles you’d have 1859 bytes.
If you had 6 lots of 6 segment tentacles you’d have 1119 bytes.
If you had 5 lots of 5 segment tentacles you’d have 794 bytes, which is getting more managable.
If you had 4 lots of 5 segment tentacles you’d have 637 bytes.

It’s doable if you keep the number down.

If I could figure out how to do atan2 then it would be possible to switch to fixed points, which would reduce the size significantly (roughly half), but that would probably be quite complicated.


#92

I’m not worried about the legality of my sprites (should I be?) because I made them from scratch and they’re half the size, but they look similar. So it’s the ethical part I’m referring to

Unless you’re very passionate don’t do it, for just an experiment I don’t think it’s necessary

Also, can I define a 2d array without having to paste ‘{’ and ‘}’ for each row ?


(Pharap) #93

No. That would make it a 1D array.

I’ll have to do it some day. But it’s too late in the day today.
I had a look and I noped out the moment I saw ‘infinite series’ mentioned.

If they’re only stylistically similar then it’s not really an issue.


#94

I might end up using that then, I don’t feel like pasting brackets everywhere and I like the way my level gets exported (I use the built-in .txt export from PyxelEdit)

I don’t know if I’ll get a lot of overhead from my tile getter this way. The formula is y * tilesPerRow + x I think, to use a 1D array as a 2D one

EDIT: I think I got it

uint8_t get_tile(uint8_t xIndex, uint8_t yIndex) {
    return level[WORLD_WIDTH* yIndex + xIndex];  
}

EDIT again:
It worked instantly, no bugs. It looks amazing https://github.com/TheProgrammer163/arduboy

Other edit:
I could use this same trick for a very huge map seperated by sections, I could use a huge 1d array and this simple formula and everything would be in the same array


(Pharap) #95

That’s what automation scripts are for :P

That’s typically called a “flat array”.

It’s quite a common thing:


#96

People often recommend Python for that, do you have other recommendations?

Honestly I’m pretty satisfied with the “flat array” (I’m wondering why it’s called that)

I’m wondering how big I can make the world before running into problems


#97

I’m in need of help with Arduboy2 Sprites functions.

First question, is there an exact equivalent to the Arduboy drawBitmap function?
Meaning, anything that’s a white pixel gets drawn, either as a black pixel or as a white pixel, and everything else is ignored. I thought drawOverwrite did that but apparently it draws the black pixels as well

I’ve been reading the sprites section of this https://mlxxxp.github.io/documents/Arduino/libraries/Arduboy2/Doxygen/pdf/Arduboy2.pdf
But I’m still confused


(Pharap) #98

Lua.

It’s by far the easiest scripting language I’ve ever tried to set up.
It compiles practically out of the box.
It’s got some nice syntax, the rules are overall quite simple (until you get into the C API and metatables, but even then it’s not too complex).

I don’t know of an easy way to register new OS-specific functions without recompiling the command line tool, but it’s got a lot of common file io functions, and modern Lua (5.2 & 5.3) supports integers and bit shifting, so it’s perfectly possible to do any kind of complex scripting that you might want to.

(And of course, you’ve already used it before, so that’s a big bonus.)

I think it’s because it was developed as a cheaper alternative to 2D arrays in languages where 2D arrays are actually arrays of references to other arrays (often called ‘jagged arrays’).
I think the ‘flattening’ was supposed to refer to the idea that you’re taking all the individual arrays and ‘flattening’ them into one.

I could be wrong, but that’s what I think I remember the explanation being.

(I think in C++ 2D arrays are implemented as ‘flat arrays’ anyway.)

It depends what the constraints of the world are.
E.g. number of tiles, can tiles be solid, can tiles be things other than solid (e.g. water, which can only be crossed under certain circumstances)

If it’s not drawOverwrite then it’s probably drawSelfMasked.

Remember that you have to prefix the image data with the width and the height.

  • drawOverwrite does no masking/transparency whatsoever.
  • drawSelfMasked draws only the white pixels
  • drawExternalMask draws the specified image with the specified mask (the mask must not have the width and height prefixed)
  • drawPlusMask draws sprites that are in a special format where the sprite is interleaved with the mask
  • drawErase I’m not sure about, I think it’s like `drawSelfMasked, but it draws the white pixels as black instead (thus ‘erasing’ the image)
    • The easiest way to find out: fill the screen with white and then try using it.

To be honest I still think the comments should be changed to use 1 instead of # and 0 instead of -, I think it’s much clearer that way.


#99

Ok, so I’m getting some weird artifacts wen drawing my player sprite. Basically, sometimes a black pixel or two will pop through

If I don’t draw anything except for my player, then this doesn’t happen.

I updated the code on GitHub sothat you can compile it and test it on the Arduboy. If you hold the A or B button the you won’t move, but you’ll still be able to change the direction you’re facing in. So no matter where you are you can test in all 4 directions easily to see if the black pixel artifact shows up.

It happens on all the dirt/grass path. Any ideas why?


(Pharap) #100

You’re trying to hack together what can only be achieved though use of a mask:

With a mask, the white bits are drawn, the black bits are transparent.
I added in new sprites rather than editing your existing ones so you could see what’s the same and what’s different.


#101

I was gonna say:

I don’t think you’ve understood what I meant. The bug only happens when walking right or walking left, and it’s only 1 pixel, but not always in the same spot that, that is black (as if a white pixel of the sprite wasn’t being drawn)

But I did what you said and it works flawlessly


(Pharap) #102

That’s because this is the sort of thing masks are designed for.
Masks are the most flexible way to implement transparency on the Arduboy.

If I knew what spr_human_outline actually looked like I could probably determine why the other approach wasn’t working.

I have many talents, but reading an array full of hexadecimal and figuring out what the original image looked like is not one of them. :P

But frankly using masks is a better option, and you only need one function instead of two, so it might just be best to switch to doing that and forget about why it was breaking before.