game is now kept in a square play area, the window can be resized and the game objects will be resized and repositioned accordingly

This commit is contained in:
Joseph Aquino 2026-01-08 21:46:37 -05:00
parent 27d074401a
commit ef0a139e70
13 changed files with 115 additions and 64 deletions

View File

@ -1,11 +1,13 @@
framerate 60 framerate 60
tickRate 60 tickRate 30
resolution 1920 1080
playerHeadColor 1 1 1 playerHeadColor 1 1 1
playerBodyColor 1 1 1 playerBodyColor 1 1 1
headGridStartPos 10 10 headGridStartPos 3 5
gridSize 50 gridCount 8

View File

@ -11,6 +11,6 @@ struct Fruit
sf::Vector2u gridPos{}; sf::Vector2u gridPos{};
Color color{}; Color color{};
sf::Vector2f windowPos(float gridSize_in) const; [[nodiscard]] sf::Vector2f windowPos(sf::Vector2f gameBoundsOrigin, float size) const;
void respawn(const std::vector<SnakeNode>& body_in, sf::Vector2u gridCount_in); void respawn(const std::vector<SnakeNode>& body_in, unsigned int gridCount_in);
}; };

View File

@ -49,6 +49,8 @@ private:
void winState(); void winState();
void setNewBounds();
private: private:
GameConfig config; GameConfig config;
@ -70,14 +72,13 @@ private:
sf::Sound moveSound; sf::Sound moveSound;
sf::RectangleShape tempRect; sf::RectangleShape tempRect;
sf::RectangleShape gameBoundry;
//sf::Sound failSound; //sf::Sound failSound;
Player player{}; Player player{};
Fruit fruit{}; Fruit fruit{};
sf::Vector2u gridCount;
size_t frameCount{}; size_t frameCount{};
size_t fruitEaten{}; size_t fruitEaten{};
GameState state{GameState::playing}; GameState state{GameState::playing};

View File

@ -11,5 +11,7 @@ struct GameConfig
Color playerHeadColor; Color playerHeadColor;
Color playerBodyColor; Color playerBodyColor;
sf::Vector2u headGridStartPos; sf::Vector2u headGridStartPos;
float gridSize; sf::Vector2u resolution;
unsigned int gridCount;
float nodeSize;
}; };

View File

@ -1,9 +0,0 @@
namespace Math
{
template<typename T, typename U, typename V>
inline T max(U first, V second)
{
return static_cast<T>(static_cast<T>(first) >= static_cast<T>(second) ? first : second);
}
}

View File

@ -14,12 +14,11 @@ enum class Direction
struct SnakeNode struct SnakeNode
{ {
SnakeNode(sf::Vector2u gridPos_in, const sf::Color& color_in = sf::Color::White); SnakeNode(sf::Vector2u gridPos_in, const sf::Color& color_in = sf::Color::White);
// sf::Vector2f windowPos{};
sf::Vector2u gridPos{}; sf::Vector2u gridPos{};
sf::Vector2u previousGridPos{}; sf::Vector2u previousGridPos{};
Color color{}; Color color{};
sf::Vector2f windowPos(float gridSize_in); [[nodiscard]] sf::Vector2f windowPos(sf::Vector2f gameBoundsOrigin, float size) const;
}; };
struct Player struct Player

View File

@ -1,5 +1,4 @@
#pragma once #pragma once
#include "Random.h" #include "Random.h"
#include "Color.h" #include "Color.h"
#include "Math-util.h"

View File

@ -9,3 +9,6 @@ if [ -z "$1" ] || [ $# -eq 0 ]
else else
../third-party/premake5/premake5 --config=$1 ecc ../third-party/premake5/premake5 --config=$1 ecc
fi fi
mv compile_commands.json ../compile_commands.json

View File

@ -1,6 +1,6 @@
#! /bin/bash #! /bin/bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
cd "$SCRIPT_DIR.." cd "$SCRIPT_DIR/.."
./bin/$1-linux-x86_64/snake ./bin/$1-linux-x86_64/snake

View File

@ -7,12 +7,12 @@ Fruit::Fruit(sf::Vector2i gridPos_in, const sf::Color& color_in)
, color(color_in) , color(color_in)
{ } { }
sf::Vector2f Fruit::windowPos(float gridSize_in) const sf::Vector2f Fruit::windowPos(sf::Vector2f gameBoundsOrigin, float size) const
{ {
return {gridPos.x * gridSize_in, gridPos.y * gridSize_in}; return {gameBoundsOrigin.x + (gridPos.x * size), gameBoundsOrigin.y + (gridPos.y * size)};
} }
void Fruit::respawn(const std::vector<SnakeNode>& body_in, sf::Vector2u gridCount_in) void Fruit::respawn(const std::vector<SnakeNode>& body_in, unsigned int gridCount_in)
{ {
unsigned int tempX; unsigned int tempX;
unsigned int tempY; unsigned int tempY;
@ -20,8 +20,8 @@ void Fruit::respawn(const std::vector<SnakeNode>& body_in, sf::Vector2u gridCoun
do do
{ {
valid = true; valid = true;
tempX = Random::get(0, gridCount_in.x - 1); tempX = Random::get(0, gridCount_in - 1);
tempY = Random::get(0, gridCount_in.y - 1); tempY = Random::get(0, gridCount_in - 1);
for (const auto& node : body_in) for (const auto& node : body_in)
{ {
if (node.gridPos.x == tempX and node.gridPos.y == tempY) if (node.gridPos.x == tempX and node.gridPos.y == tempY)

View File

@ -12,7 +12,6 @@
Game::Game(bool useImgui_in) Game::Game(bool useImgui_in)
: config() : config()
, window({sf::VideoMode({1920u, 1080u}), "project-snake"})
, score(font) , score(font)
, failSound(failSoundBuffer) , failSound(failSoundBuffer)
, winSound(winSoundBuffer) , winSound(winSoundBuffer)
@ -21,26 +20,28 @@ Game::Game(bool useImgui_in)
, moveSound(moveSoundBuffer) , moveSound(moveSoundBuffer)
, useImgui(useImgui_in) , useImgui(useImgui_in)
{ {
if (!parseConfigFile())
{
std::cout << "ERROR: Could not parse config file\n";
return;
}
window.create(sf::VideoMode(config.resolution), "project-snake", sf::Style::Resize | sf::Style::Close);
if (!ImGui::SFML::Init(window)) if (!ImGui::SFML::Init(window))
{ {
std::cout << "ERROR: Could not open window\n"; std::cout << "ERROR: Could not open window\n";
return; return;
} }
if (!parseConfigFile()) gameBoundry.setFillColor(sf::Color::Transparent);
{ gameBoundry.setOutlineColor(sf::Color::White);
std::cout << "ERROR: Could not parse config file\n"; setNewBounds();
window.close();
return;
}
gridCount.x = window.getSize().x / static_cast<unsigned int>(config.gridSize); player.body.reserve(config.gridCount * config.gridCount);
gridCount.y = window.getSize().y / static_cast<unsigned int>(config.gridSize);
player.body.reserve(100);
player.body.emplace_back(config.headGridStartPos); player.body.emplace_back(config.headGridStartPos);
fruit.gridPos = {5, 5}; fruit.respawn(player.body, config.gridCount);
fruit.color = sf::Color::Red; fruit.color = sf::Color::Red;
if (!font.openFromFile("assets/fonts/ChakraPetch-Regular.ttf")) if (!font.openFromFile("assets/fonts/ChakraPetch-Regular.ttf"))
@ -79,11 +80,11 @@ Game::Game(bool useImgui_in)
return; return;
} }
failSound.setVolume(10.f); failSound.setVolume(config.volume);
winSound.setVolume(10.f); winSound.setVolume(config.volume);
bgMusic.setVolume(10.f); bgMusic.setVolume(config.volume);
eatSound.setVolume(10.f); eatSound.setVolume(config.volume);
moveSound.setVolume(10.f); moveSound.setVolume(config.volume);
bgMusic.setLooping(true); bgMusic.setLooping(true);
bgMusic.play(); bgMusic.play();
@ -96,11 +97,13 @@ Game::Game(bool useImgui_in)
player.body.emplace_back(sf::Vector2i{config.headGridStartPos.x - 4, config.headGridStartPos.y}); player.body.emplace_back(sf::Vector2i{config.headGridStartPos.x - 4, config.headGridStartPos.y});
*/ */
tempRect.setSize(sf::Vector2f{(config.gridSize), config.gridSize}); tempRect.setSize(sf::Vector2f{config.nodeSize, config.nodeSize});
tempRect.setOutlineThickness(-5); tempRect.setOutlineThickness((config.nodeSize / 10.f) * -1);
tempRect.setOutlineColor(sf::Color::Black); tempRect.setOutlineColor(sf::Color::Black);
score.setPosition({10, static_cast<float>(window.getSize().y - 40)}); score.setPosition({10, static_cast<float>(window.getSize().y - 40)});
score.setOutlineColor(sf::Color::Black);
score.setOutlineThickness(-1.f);
window.setFramerateLimit(config.framerate); window.setFramerateLimit(config.framerate);
} }
@ -148,9 +151,15 @@ bool Game::parseConfigFile()
continue; continue;
} }
if (inputBuff == "gridSize") if (inputBuff == "gridCount")
{ {
configFile >> config.gridSize; configFile >> config.gridCount;
continue;
}
if (inputBuff == "resolution")
{
configFile >> config.resolution.x >> config.resolution.y;
continue; continue;
} }
} }
@ -234,6 +243,11 @@ void Game::input()
break; break;
} }
} }
if (const auto* windowResized = event->getIf<sf::Event::Resized>())
{
setNewBounds();
}
} }
} }
@ -253,11 +267,11 @@ void Game::collision()
if (player.head().gridPos == fruit.gridPos) if (player.head().gridPos == fruit.gridPos)
{ {
player.body.emplace_back(player.body.back().previousGridPos); player.body.emplace_back(player.body.back().previousGridPos);
if (player.body.size() != (gridCount.x * gridCount.y)) if (player.body.size() != (config.gridCount * config.gridCount))
{ {
eatSound.play(); eatSound.play();
player.score += 100; player.score += 100;
fruit.respawn(player.body, gridCount); fruit.respawn(player.body, config.gridCount);
} }
else else
{ {
@ -312,7 +326,7 @@ void Game::movement()
break; break;
case Direction::down: case Direction::down:
if (head.gridPos.y == gridCount.y - 1) if (head.gridPos.y == config.gridCount - 1)
{ {
state = GameState::lose; state = GameState::lose;
failSound.play(); failSound.play();
@ -340,7 +354,7 @@ void Game::movement()
break; break;
case Direction::right: case Direction::right:
if (head.gridPos.x == gridCount.x -1) if (head.gridPos.x == config.gridCount -1)
{ {
state = GameState::lose; state = GameState::lose;
failSound.play(); failSound.play();
@ -365,8 +379,8 @@ void Game::movement()
void Game::imgui() void Game::imgui()
{ {
ImGui::SetNextWindowCollapsed(true, ImGuiCond_Once); ImGui::SetNextWindowCollapsed(false, ImGuiCond_Once);
ImGui::SetNextWindowSize({490,375}, ImGuiCond_Once); ImGui::SetNextWindowSize({369,374}, ImGuiCond_Once);
ImGui::SetNextWindowPos({26,29}, ImGuiCond_Once); ImGui::SetNextWindowPos({26,29}, ImGuiCond_Once);
if(!ImGui::Begin("menu")) if(!ImGui::Begin("menu"))
{ {
@ -375,13 +389,17 @@ void Game::imgui()
} }
ImGui::Text("Controls:"); ImGui::Text("Controls:");
ImGui::Indent(); ImGui::Indent();
ImGui::Text("A/D or arrow keys: move left and right"); ImGui::Text("W/A/S/D or arrow keys: move");
ImGui::Text("R: reset game"); ImGui::Text("R: reset game");
ImGui::Unindent(); ImGui::Unindent();
if (ImGui::SliderFloat("Game Volume", &config.volume, 0, 100, "%.1f")) if (ImGui::SliderFloat("Game Volume", &config.volume, 0, 100, "%.1f"))
{ {
failSound.setVolume(config.volume);
winSound.setVolume(config.volume);
bgMusic.setVolume(config.volume);
eatSound.setVolume(config.volume);
moveSound.setVolume(config.volume);
} }
@ -395,14 +413,16 @@ void Game::render()
for (auto& node : player.body) for (auto& node : player.body)
{ {
tempRect.setFillColor(node.color.sfml()); tempRect.setFillColor(node.color.sfml());
tempRect.setPosition(node.windowPos(config.gridSize)); tempRect.setPosition(node.windowPos(gameBoundry.getPosition(), config.nodeSize));
window.draw(tempRect); window.draw(tempRect);
} }
tempRect.setFillColor(fruit.color.sfml()); tempRect.setFillColor(fruit.color.sfml());
tempRect.setPosition(fruit.windowPos(config.gridSize)); tempRect.setPosition(fruit.windowPos(gameBoundry.getPosition(), config.nodeSize));
window.draw(tempRect); window.draw(tempRect);
window.draw(gameBoundry);
score.setString("Score: " + std::to_string(player.score)); score.setString("Score: " + std::to_string(player.score));
window.draw(score); window.draw(score);
@ -425,11 +445,17 @@ void Game::soundSystem()
void Game::resetGame() void Game::resetGame()
{ {
setNewBounds();
player.body.clear(); player.body.clear();
player.body.emplace_back(config.headGridStartPos); player.body.emplace_back(config.headGridStartPos);
player.facing = Direction::up; player.facing = Direction::up;
player.inputBuffer = {};
player.score = 0;
fruit.respawn(player.body, gridCount); fruit.respawn(player.body, config.gridCount);
frameCount = 0;
state = GameState::playing; state = GameState::playing;
bgMusic.play(); bgMusic.play();
@ -453,6 +479,37 @@ void Game::winState()
} }
} }
void Game::setNewBounds()
{
const sf::Vector2f newSize(window.getSize());
const float minDimension = std::min(newSize.x, newSize.y);
const sf::FloatRect visibleArea({0.f, 0.f} , newSize);
window.setView(sf::View(visibleArea));
// how big each node is
// use integer division to cut off remainder
config.nodeSize = minDimension / config.gridCount;
const float boundryThickness = config.nodeSize * 0.05f * -1;
tempRect.setSize(sf::Vector2f{config.nodeSize, config.nodeSize});
tempRect.setOutlineThickness((config.nodeSize / 10.f) * -1);
const float boundarySize = config.gridCount * config.nodeSize;
// Center the grid in the window
const float boundaryPosX = (newSize.x - boundarySize) / 2.f;
const float boundaryPosY = (newSize.y - boundarySize) / 2.f;
gameBoundry.setSize({boundarySize, boundarySize});
gameBoundry.setPosition({boundaryPosX, boundaryPosY});
gameBoundry.setOutlineThickness(boundryThickness);
score.setPosition({10, newSize.y - 40});
}
void Game::run() void Game::run()
{ {
while(window.isOpen()) while(window.isOpen())

View File

@ -5,9 +5,9 @@ SnakeNode::SnakeNode(sf::Vector2u gridPos_in, const sf::Color& color_in)
, color(color_in) , color(color_in)
{ } { }
sf::Vector2f SnakeNode::windowPos(float gridSize_in) sf::Vector2f SnakeNode::windowPos(const sf::Vector2f gameBoundsOrigin, float size) const
{ {
return {gridPos.x * gridSize_in, gridPos.y * gridSize_in}; return {gameBoundsOrigin.x + (gridPos.x * size), gameBoundsOrigin.y + (gridPos.y * size)};
} }
SnakeNode& Player::head() SnakeNode& Player::head()

View File

@ -14,9 +14,6 @@ int main(int argc, char* argv[])
} }
LOG("\n\n\033[32mTODO: "); LOG("\n\n\033[32mTODO: ");
LOG("-Implement fruit eating");
LOG("-Implement sound effects and background music");
LOG("-Handle game over state");
LOG("-Implement game config via imgui"); LOG("-Implement game config via imgui");
LOG("\033[0m"); LOG("\033[0m");