Het is even geleden dat ik gewerkt heb in C++, en juist daardoor kreeg ik weer motivatie om hierin een spelletje te maken. Ik heb naar een library gezocht om te gebruiken hiervoor, en heb besloten SFML te gebruiken.
Een game is nooit af, er waren nog genoeg dingen die ik wilde veranderen. Maar uiteindelijk werd dit de laatste versie:
In het spel kun je Super Sonic worden wanneer je minstens 7 chaos emeralds en 50 ringen heb veranderd. Terwijl je springt (zodra je valt is het al te laat) zul je snel “supersonic” of “super sonic” moeten typen om te veranderen.
Note: Je hebt Visual C++ Redistributable Packages for Visual Studio 2013 x86 nodig.
Threading
Dit was mijn eerste SFML game, dus ik heb de tutorials op de website gevolgd.
Al snel kwam ik een uitleg tegen over threading, iets waar ik wel al over gehoord had, maar nog nooit naar gekeken heb. Na een kleine uitleg te hebben gekregen van SFML besloot ik om het uit te proberen in dit spel. SFML heeft zijn eigen threading class, maar raad aan zo mogelijk STD::Thread te gebruiken. Na het vinden van een voorbeeld begon ik het te gebruiken.
std::thread drawThread(Draw, &window);
Door threads te gebruiken, vroeg ik me al af of het geen probleem zou veroorzaken wanneer ik dezelfde objecten zou gebruiken in mijn 2 threads. In het begin leek het geen probleem te zijn, maar later kreeg ik “access violation” errors op willekeurige momenten. Toen leerde ik gebruik te maken van een mutex.
obstaclesMutex.lock(); for (std::vector<Obstacle*>::iterator it = obstacles.begin(); it != obstacles.end(); ++it) (*it)->Draw(window); obstaclesMutex.unlock();
Animation
De eerste stap in SFML was het krijgen van een sprite op het scherm, dat was lekker snel gedaan. Maar SFML heeft niet iets voor animations zoals ik gewent was in unity. Ik heb dan ook zelf een functie moeten schrijven om animaties mogelijk te maken.
void Player::UpdateAnimation(Time elaspedTime) { spriteUpdateTimer += elaspedTime.asSeconds(); if (spriteUpdateTimer >= spriteUpdateTime) { spriteUpdateTimer = 0.f; // if spritePos is higher than the frameamount go back to the first frame spritePos >= spriteStartingPoint.x + frameAmount - 1 ? spritePos = spriteStartingPoint.x : spritePos++; sprite.setTextureRect(IntRect( /* X */ spritePos * spriteSize.x, /* Y */ spriteStartingPoint.y * spriteSize.y, /* W */ spriteSize.x, /* H */ spriteSize.y)); } }
Ground
Ook moest ik grond genereren voor Sonic om op te rollen, eerst wilde ik een convex shape maken, zodat de berg willekeurig wat hoger en lager ging.
/* For every point in the world X Position stays the same, but every point (except the ones that are number maxGroundPoints & higher) will take the Y position of the next one. The last one (maxGroundPoint-1) will get a random new position. */ updateTimer -= elaspedTime.asSeconds(); if (updateTimer <= 0) { for (int i = 0; i < maxGroundPoints; i++) { if (i != maxGroundPoints - 1) ground.setPoint(i, Vector2f(ground.getPoint(i).x, ground.getPoint(i + 1).y - steepness)); else { float y = getNextYChange(ground.getPoint(i).y); ground.setPoint(i, Vector2f(ground.getPoint(i).x, ground.getPoint(i).y + y)); } } updateTimer += updateTime; }
Echter kreeg ik vaak misvormingen in de convexshape, en het werkte gewoon niet helemaal lekker. Uiteindelijk heb ik maar besloten een rechte berg te maken doormiddel van sprites in een multi dimensionele vector.
for (int x = 0; x < ground.size(); x++) { for (int y = 0; y < ground[x].size(); y++) { // get the current position and calculate the x/y change that needs to be added Vector2f curPos = ground[x][y].getPosition(); Vector2f change = Vector2f(spriteSize.x * speed * -1, spriteSize.y * speed * -1); // Make sure the ground tile is in-screen, else move it to the right if (curPos.x > 0 - spriteSize.x * 2) // move to left { // if this tile is the first in line(most left) just add the change to it if (x == firstX) { ground[x][y].setPosition(curPos + change * elaspedTime.asSeconds()); } else { // if it's not the first in line get the one before it. int getX = x - 1; if (getX < 0) getX = ground.size() - 1; ground[x][y].setPosition(ground[getX][y].getPosition() + spriteSize); } } else // teleport to the right side { int lastX = x - 1; if (lastX < 0) lastX = ground.size() - 1; ground[x][y].setPosition(ground[lastX][y].getPosition() + spriteSize); firstX = x + 1; if (firstX >= ground.size()) firstX = 0; break; } } }
Collisions
I had to check collisions for the obstacles and the ground. The obstacles return a string that’s combined with them and linked to player actions. (At the time of writing this is either an obstacle that makes the player jump or an obstacle that requires no action) the string it returns is what the player types to make it happen.
Ik moest natuurlijk checken op collision met de obstakels en de grond. In de collision geven obstakels een string terug, wat gecombineerd is aan een actie die de speler dan uitvoerd. (Op het moment van schrijven is dit een obstakel wat de speler laat springen, of een obstakel wat geen actie terug geeft zoals een ring. ) Als er een string wordt terug gegeven moet de speler deze typen om de actie daadwerkelijk te doen.
Ook gebruikte ik een nieuwe manier van loopen (iterator). Klik hier voor het voorbeeld dat ik gebruikt heb.
std::string ObstacleSpawner::CheckObstacleCollision(Vector2f position, Vector2f size) { for (std::vector<Obstacle*>::iterator it = obstacles.begin(); it != obstacles.end(); ++it) { Vector2f oPos = (*it)->GetPosition(); if (position.x > oPos.x - size.x && position.y > oPos.y - size.y) { if (position.x < oPos.x + size.x && position.y < oPos.y + size.y) { (*it)->hit = true; if ((*it)->audio != "" && playSfx) { sb.loadFromFile((*it)->audio); sound.setBuffer(sb); sound.play(); } return (*it)->GetString(); } } } return ""; }
For the ground collision (used to know when player is back on the ground after jumping) it’s different. The ground sprites form a triangle, so to check for collision I draw a triangle based on the player x point.
Voor de collision met de grond (nodig voor wanneer de speler springt en weer valt naar de grond) tekende ik een extra driehoekje die de grond voorstelt. De driehoek wordt getekent op basis van de spelers x-as.
Vector2f Ground::UpdateTriangle(float x) { // http://stackoverflow.com/questions/1727881/how-to-use-the-pi-constant-in-c float pi = std::atanf(1.0) * 4; // lijn schuine zijde / line hypotenuse | rotation: 45 debuglines[0].setSize(Vector2f(x / sinf(45 * pi / 180), 5)); // overstaande zijde / opposite side | rotation: 90 debuglines[1].setSize(Vector2f(x * tanf(45 * pi / 180), 5)); // aanliggende zijde / adjacent side | rotation: 0 debuglines[2].setSize(Vector2f(x, 5)); debuglines[2].setPosition(Vector2f(5, debuglines[1].getSize().x + groundYstartingPoint)); return Vector2f(debuglines[1].getPosition().x + debuglines[1].getSize().x, debuglines[2].getPosition().x + debuglines[2].getSize().x); }
Settings.ini
School wilde graag dat ik een opties file aan een game van mij toevoeg. Op dat moment was ik bezig met deze game, en het leek mij wel handig zodat mensen dingen als de resolutie kunnen veranderen.
std::string ExePath() // http://stackoverflow.com/questions/875249/how-to-get-current-directory { char * buffer = new char[MAX_PATH]; GetModuleFileNameA(NULL, buffer, MAX_PATH); std::string::size_type pos = std::string(buffer).find_last_of("\\/"); return std::string(buffer).substr(0, pos); } void LoadConfig() { // I'm using microsoft docs here http://msdn.microsoft.com/en-us/library/windows/desktop/ms724345(v=vs.85).aspx // According to microsoft docs it'll use windows directory as default so I need to get the path of the executable. std::string file = ExePath() + "\\settings.ini"; windowSize.x = GetPrivateProfileIntA("SCREEN", "width", windowSize.x, file.c_str()); windowSize.y = GetPrivateProfileIntA("SCREEN", "height", windowSize.y, file.c_str()); char* result = new char[255]; GetPrivateProfileStringA("SCREEN", "fullscreen", "false", result, 255, file.c_str()); if (strcmp(result, "true") == 0) windowStyle = Style::Fullscreen; GetPrivateProfileStringA("SOUND", "bgm", "on", result, 255, file.c_str()); if (strcmp(result, "off") == 0) playbgm = false; GetPrivateProfileStringA("SOUND", "sfx", "on", result, 255, file.c_str()); if (strcmp(result, "off") == 0) playsfx = false; GetPrivateProfileStringA("DEBUG", "groundcollision", "on", result, 255, file.c_str()); if (strcmp(result, "on") == 0) ground.showDebug = true; }
Overriding Player Actions
Toen ik wilde dat de speler kon springen gebruikte ik een enum van de speler acties en een lange functie dat elk van deze actie update. Dit alles in de player class, het werd best wel groot. Ik heb toen besloten een simpele class te maken, om player acties van te maken.
#pragma once #include <SFML/Graphics.hpp> class Player; using namespace sf; class PlayerAction { public: PlayerAction(); virtual void Update(Time elaspedTime) { } Player * player; };
Op basis van deze class heb ik mijn player-actions gemaakt. Met elk een eigen update i.p.v 1 grote functie.
#pragma once #include "PlayerAction.h" using namespace sf; class RollingAction : public PlayerAction { public: RollingAction(Player * p); void Update(Time elaspedTime); };