421 lines
12 KiB
C++
421 lines
12 KiB
C++
#include <SFML/Graphics.hpp>
|
|
#include <SFML/Audio.hpp>
|
|
#include <vector>
|
|
#include <random>
|
|
#include <array>
|
|
|
|
// Set namespaces for SFML and STD
|
|
using namespace sf;
|
|
using namespace std;
|
|
|
|
// Init window / sprite variables ( constant and will never change )
|
|
constexpr int WIDTH = 640;
|
|
constexpr int HEIGHT = 480;
|
|
constexpr int GRID_SIZE = 20;
|
|
constexpr int GRID_WIDTH = WIDTH / GRID_SIZE;
|
|
constexpr int GRID_HEIGHT = HEIGHT / GRID_SIZE;
|
|
constexpr int SPRITE_SIZE = GRID_SIZE;
|
|
|
|
struct SnakeSegment {
|
|
int x, y;
|
|
};
|
|
|
|
class NumericSprite {
|
|
private:
|
|
Texture texture;
|
|
Sprite sprite;
|
|
|
|
public:
|
|
NumericSprite() {
|
|
texture.create(SPRITE_SIZE, SPRITE_SIZE);
|
|
sprite.setTexture(texture);
|
|
}
|
|
|
|
void createFromPattern(const array<array<int, 5>, 5>& pattern, Color color) {
|
|
Image img;
|
|
img.create(SPRITE_SIZE, SPRITE_SIZE, Color::Transparent);
|
|
|
|
int scale = SPRITE_SIZE / 5;
|
|
for (int y = 0; y < 5; y++) {
|
|
for (int x = 0; x < 5; x++) {
|
|
if (pattern[y][x] == 1) {
|
|
for (int sy = 0; sy < scale; sy++) {
|
|
for (int sx = 0; sx < scale; sx++) {
|
|
img.setPixel(x * scale + sx, y * scale + sy, color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
texture.update(img);
|
|
}
|
|
|
|
const Sprite& getSprite() const { return sprite; }
|
|
void setPosition(float x, float y) { sprite.setPosition(x, y); }
|
|
};
|
|
|
|
class SoundManager {
|
|
private:
|
|
SoundBuffer moveBuffer;
|
|
SoundBuffer eatBuffer;
|
|
SoundBuffer gameOverBuffer;
|
|
Sound moveSound;
|
|
Sound eatSound;
|
|
Sound gameOverSound;
|
|
bool soundLoaded;
|
|
|
|
public:
|
|
SoundManager() : soundLoaded(true) {
|
|
// Create simple beep sounds programmatically
|
|
createMoveSound();
|
|
createEatSound();
|
|
createGameOverSound();
|
|
|
|
// Set volumes to be quieter
|
|
moveSound.setVolume(15.0f); // Very quiet for movement
|
|
eatSound.setVolume(25.0f); // Bit louder for eating
|
|
gameOverSound.setVolume(30.0f); // Louder for game over
|
|
}
|
|
|
|
void createMoveSound() {
|
|
const unsigned SAMPLE_RATE = 44100;
|
|
const unsigned DURATION = 5000; // Very short duration (in samples)
|
|
|
|
std::vector<Int16> samples(DURATION);
|
|
|
|
// Generate a simple beep for movement (higher pitch, shorter)
|
|
for (unsigned i = 0; i < DURATION; i++) {
|
|
double time = i / static_cast<double>(SAMPLE_RATE);
|
|
samples[i] = 10000 * sin(2 * 3.14159 * 880 * time); // 880 Hz tone
|
|
}
|
|
|
|
// Apply fade-out
|
|
for (unsigned i = DURATION / 2; i < DURATION; i++) {
|
|
double fadeOut = 1.0 - (static_cast<double>(i - DURATION / 2) / (DURATION / 2));
|
|
samples[i] = static_cast<Int16>(samples[i] * fadeOut);
|
|
}
|
|
|
|
if (!moveBuffer.loadFromSamples(&samples[0], samples.size(), 1, SAMPLE_RATE)) {
|
|
soundLoaded = false;
|
|
}
|
|
moveSound.setBuffer(moveBuffer);
|
|
}
|
|
|
|
void createEatSound() {
|
|
const unsigned SAMPLE_RATE = 44100;
|
|
const unsigned DURATION = 8000; // Slightly longer duration for eat sound
|
|
|
|
std::vector<Int16> samples(DURATION);
|
|
|
|
// Generate eat sound (rising pitch)
|
|
for (unsigned i = 0; i < DURATION; i++) {
|
|
double time = i / static_cast<double>(SAMPLE_RATE);
|
|
double frequency = 440 + (i * 400 / DURATION); // Rising from 440Hz to 840Hz
|
|
samples[i] = 12000 * sin(2 * 3.14159 * frequency * time);
|
|
}
|
|
|
|
// Apply fade-in and fade-out
|
|
for (unsigned i = 0; i < DURATION / 4; i++) {
|
|
double fadeIn = static_cast<double>(i) / (DURATION / 4);
|
|
samples[i] = static_cast<Int16>(samples[i] * fadeIn);
|
|
}
|
|
|
|
for (unsigned i = 3 * DURATION / 4; i < DURATION; i++) {
|
|
double fadeOut = 1.0 - (static_cast<double>(i - 3 * DURATION / 4) / (DURATION / 4));
|
|
samples[i] = static_cast<Int16>(samples[i] * fadeOut);
|
|
}
|
|
|
|
if (!eatBuffer.loadFromSamples(&samples[0], samples.size(), 1, SAMPLE_RATE)) {
|
|
soundLoaded = false;
|
|
}
|
|
eatSound.setBuffer(eatBuffer);
|
|
}
|
|
|
|
void createGameOverSound() {
|
|
const unsigned SAMPLE_RATE = 44100;
|
|
const unsigned DURATION = 44100 / 2; // Half second
|
|
|
|
std::vector<Int16> samples(DURATION);
|
|
|
|
// Generate game over sound (descending pitch)
|
|
for (unsigned i = 0; i < DURATION; i++) {
|
|
double time = i / static_cast<double>(SAMPLE_RATE);
|
|
double frequency = 440 - (i * 200 / DURATION); // Falling from 440Hz to 240Hz
|
|
samples[i] = 15000 * sin(2 * 3.14159 * frequency * time);
|
|
}
|
|
|
|
// Apply "wah wah" effect with fading
|
|
for (unsigned i = 0; i < DURATION; i++) {
|
|
double envelope = 1.0 - 0.8 * (static_cast<double>(i) / DURATION);
|
|
samples[i] = static_cast<Int16>(samples[i] * envelope);
|
|
}
|
|
|
|
if (!gameOverBuffer.loadFromSamples(&samples[0], samples.size(), 1, SAMPLE_RATE)) {
|
|
soundLoaded = false;
|
|
}
|
|
gameOverSound.setBuffer(gameOverBuffer);
|
|
}
|
|
|
|
void playMoveSound() {
|
|
if (soundLoaded) {
|
|
moveSound.play();
|
|
}
|
|
}
|
|
|
|
void playEatSound() {
|
|
if (soundLoaded) {
|
|
eatSound.play();
|
|
}
|
|
}
|
|
|
|
void playGameOverSound() {
|
|
if (soundLoaded) {
|
|
gameOverSound.play();
|
|
}
|
|
}
|
|
};
|
|
|
|
class Game {
|
|
private:
|
|
RenderWindow window;
|
|
vector<SnakeSegment> snake;
|
|
Vector2f fruit;
|
|
Vector2f direction;
|
|
bool isMoving;
|
|
Clock clock;
|
|
float speed;
|
|
int score;
|
|
Font font;
|
|
Text scoreText;
|
|
SoundManager soundManager;
|
|
Vector2f lastDirection;
|
|
|
|
// Numeric sprites
|
|
NumericSprite snakeHeadSprite;
|
|
NumericSprite snakeBodySprite;
|
|
NumericSprite fruitSprite;
|
|
|
|
// Grid vertex array
|
|
VertexArray grid;
|
|
|
|
// Patterns
|
|
const array<array<int, 5>, 5> HEAD_PATTERN = {{
|
|
{1, 1, 1, 1, 1},
|
|
{1, 1, 1, 1, 1},
|
|
{1, 1, 1, 1, 1},
|
|
{1, 1, 1, 1, 1},
|
|
{1, 1, 1, 1, 1}
|
|
}};
|
|
|
|
const array<array<int, 5>, 5> BODY_PATTERN = {{
|
|
{1, 1, 1, 1, 1},
|
|
{1, 1, 1, 1, 1},
|
|
{1, 1, 1, 1, 1},
|
|
{1, 1, 1, 1, 1},
|
|
{1, 1, 1, 1, 1}
|
|
}};
|
|
|
|
// Apple-like pattern
|
|
const array<array<int, 5>, 5> FRUIT_PATTERN = {{
|
|
{0, 1, 1, 1, 0},
|
|
{1, 1, 1, 1, 1},
|
|
{1, 1, 1, 1, 1},
|
|
{1, 1, 1, 1, 1},
|
|
{0, 1, 1, 1, 0}
|
|
}};
|
|
|
|
public:
|
|
Game() : window(VideoMode(WIDTH, HEIGHT), "SFML CPPSNEK V1.3 With Sound"), direction(1, 0),
|
|
isMoving(false), speed(0.1f), score(0), grid(Lines), lastDirection(1, 0) {
|
|
window.setFramerateLimit(60);
|
|
|
|
// Initialize snake
|
|
snake.push_back({GRID_WIDTH / 2, GRID_HEIGHT / 2});
|
|
snake.push_back({GRID_WIDTH / 2 - 1, GRID_HEIGHT / 2});
|
|
snake.push_back({GRID_WIDTH / 2 - 2, GRID_HEIGHT / 2});
|
|
|
|
// Create sprites
|
|
snakeHeadSprite.createFromPattern(HEAD_PATTERN, Color(200, 200, 0)); // Yellow head
|
|
snakeBodySprite.createFromPattern(BODY_PATTERN, Color(0, 200, 0)); // Green body
|
|
fruitSprite.createFromPattern(FRUIT_PATTERN, Color(200, 0, 0)); // Red apple
|
|
|
|
// Create grid
|
|
createGrid();
|
|
|
|
spawnFruit();
|
|
|
|
// Setup font
|
|
if (!font.loadFromFile("/usr/share/fonts/gnu-free/FreeSans.ttf")) {
|
|
font.loadFromFile("/usr/share/fonts/liberation/LiberationSans-Regular.ttf");
|
|
}
|
|
scoreText.setFont(font);
|
|
scoreText.setCharacterSize(24);
|
|
scoreText.setFillColor(Color::White);
|
|
scoreText.setPosition(10, 10);
|
|
updateScoreText();
|
|
}
|
|
|
|
void createGrid() {
|
|
// Vertical lines
|
|
for (int x = 0; x <= WIDTH; x += GRID_SIZE) {
|
|
grid.append(Vertex(Vector2f(x, 0), Color(50, 50, 50)));
|
|
grid.append(Vertex(Vector2f(x, HEIGHT), Color(50, 50, 50)));
|
|
}
|
|
|
|
// Horizontal lines
|
|
for (int y = 0; y <= HEIGHT; y += GRID_SIZE) {
|
|
grid.append(Vertex(Vector2f(0, y), Color(50, 50, 50)));
|
|
grid.append(Vertex(Vector2f(WIDTH, y), Color(50, 50, 50)));
|
|
}
|
|
}
|
|
|
|
void spawnFruit() {
|
|
static random_device rd;
|
|
static mt19937 gen(rd());
|
|
uniform_int_distribution<> distX(0, GRID_WIDTH - 1);
|
|
uniform_int_distribution<> distY(0, GRID_HEIGHT - 1);
|
|
|
|
fruit.x = distX(gen);
|
|
fruit.y = distY(gen);
|
|
|
|
for (const auto& segment : snake) {
|
|
if (segment.x == fruit.x && segment.y == fruit.y) {
|
|
spawnFruit();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void updateScoreText() {
|
|
scoreText.setString("Score: " + to_string(score));
|
|
}
|
|
|
|
void handleInput() {
|
|
bool directionChanged = false;
|
|
|
|
if (Keyboard::isKeyPressed(Keyboard::Left) && direction.x == 0) {
|
|
direction = Vector2f(-1, 0);
|
|
isMoving = true;
|
|
directionChanged = true;
|
|
}
|
|
else if (Keyboard::isKeyPressed(Keyboard::Right) && direction.x == 0) {
|
|
direction = Vector2f(1, 0);
|
|
isMoving = true;
|
|
directionChanged = true;
|
|
}
|
|
else if (Keyboard::isKeyPressed(Keyboard::Up) && direction.y == 0) {
|
|
direction = Vector2f(0, -1);
|
|
isMoving = true;
|
|
directionChanged = true;
|
|
}
|
|
else if (Keyboard::isKeyPressed(Keyboard::Down) && direction.y == 0) {
|
|
direction = Vector2f(0, 1);
|
|
isMoving = true;
|
|
directionChanged = true;
|
|
}
|
|
|
|
if (directionChanged && (direction.x != lastDirection.x || direction.y != lastDirection.y)) {
|
|
soundManager.playMoveSound();
|
|
lastDirection = direction;
|
|
}
|
|
}
|
|
|
|
void update() {
|
|
if (clock.getElapsedTime().asSeconds() < speed || !isMoving)
|
|
return;
|
|
|
|
clock.restart();
|
|
|
|
SnakeSegment newHead = {snake[0].x + direction.x, snake[0].y + direction.y};
|
|
|
|
if (newHead.x < 0 || newHead.x >= GRID_WIDTH || newHead.y < 0 || newHead.y >= GRID_HEIGHT) {
|
|
soundManager.playGameOverSound();
|
|
reset();
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 1; i < snake.size(); i++) {
|
|
if (newHead.x == snake[i].x && newHead.y == snake[i].y) {
|
|
soundManager.playGameOverSound();
|
|
reset();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (newHead.x == fruit.x && newHead.y == fruit.y) {
|
|
snake.insert(snake.begin(), newHead);
|
|
spawnFruit();
|
|
score += 10;
|
|
updateScoreText();
|
|
soundManager.playEatSound();
|
|
|
|
if (score % 50 == 0 && speed > 0.05f) {
|
|
speed -= 0.01f;
|
|
}
|
|
} else {
|
|
snake.insert(snake.begin(), newHead);
|
|
snake.pop_back();
|
|
}
|
|
}
|
|
|
|
void reset() {
|
|
snake.clear();
|
|
snake.push_back({GRID_WIDTH / 2, GRID_HEIGHT / 2});
|
|
snake.push_back({GRID_WIDTH / 2 - 1, GRID_HEIGHT / 2});
|
|
snake.push_back({GRID_WIDTH / 2 - 2, GRID_HEIGHT / 2});
|
|
direction = Vector2f(1, 0);
|
|
lastDirection = Vector2f(1, 0);
|
|
isMoving = false;
|
|
speed = 0.1f;
|
|
score = 0;
|
|
updateScoreText();
|
|
spawnFruit();
|
|
}
|
|
|
|
void render() {
|
|
window.clear(Color(30, 30, 30)); // Dark gray background
|
|
|
|
// Draw grid first
|
|
window.draw(grid);
|
|
|
|
// Draw snake
|
|
for (size_t i = 0; i < snake.size(); i++) {
|
|
if (i == 0) {
|
|
snakeHeadSprite.setPosition(snake[i].x * GRID_SIZE, snake[i].y * GRID_SIZE);
|
|
window.draw(snakeHeadSprite.getSprite());
|
|
} else {
|
|
snakeBodySprite.setPosition(snake[i].x * GRID_SIZE, snake[i].y * GRID_SIZE);
|
|
window.draw(snakeBodySprite.getSprite());
|
|
}
|
|
}
|
|
|
|
// Draw fruit (apple)
|
|
fruitSprite.setPosition(fruit.x * GRID_SIZE, fruit.y * GRID_SIZE);
|
|
window.draw(fruitSprite.getSprite());
|
|
|
|
// Draw score
|
|
window.draw(scoreText);
|
|
|
|
window.display();
|
|
}
|
|
|
|
void run() {
|
|
while (window.isOpen()) {
|
|
Event event;
|
|
while (window.pollEvent(event)) {
|
|
if (event.type == Event::Closed)
|
|
window.close();
|
|
}
|
|
|
|
handleInput();
|
|
update();
|
|
render();
|
|
}
|
|
}
|
|
};
|
|
|
|
int main() {
|
|
Game game;
|
|
game.run();
|
|
return 0;
|
|
} |