Code of Poem
Unknown programmer's programming note.

ブロック崩しを作る (5) - 階層構造にしたバージョンのコード

下のスクリーンショットのようなGodotのノードによる階層的なゲームのシーンの表現をイメージして作り直したコードです。

Bricks 5 Godot Scene Node

クラス導入前のコードクラスを導入してフラットな構造にしたコードをベースに書き直しました。

#include <SDL2/SDL.h>
#include <vector>
#include <string>
#include <algorithm>
using std::vector;
using std::string;
using std::remove_if;
using std::copy_if;

const int ScreenWidth = 400;
const int ScreenHeight = 400;

class Node;

void resolve_collision(Node* node, Node* root);
bool overlaps_x(const SDL_Rect& a, const SDL_Rect& b);
bool overlaps_y(const SDL_Rect& a, const SDL_Rect& b);
bool is_dead(const Node* node);

struct Vec2 {
    double x;
    double y;
};

Vec2 normalized(const Vec2& v)
{
    double length = sqrt(v.x * v.x + v.y * v.y);
    return Vec2{v.x / length, v.y / length};
}

Vec2 scalar_multiplied(const Vec2& v, double s)
{
    return Vec2{v.x * s, v.y * s};
}

class Node {
public:
    enum State { Active, Dead };

    Node(const string& tag, const SDL_Rect& rect, const SDL_Color& color);
    virtual ~Node();
    void update(Node* root);
    void render(SDL_Renderer* renderer);
    virtual void on_collision_enter(Node* other);
    void add_subnode(Node* subnode);
    Node* find_subnode_by_tag(const string& tag);
    void kill_dead_subnodes();

    string tag() const { return tag_; }
    SDL_Rect rect() const { return rect_; }
    SDL_Color color() const { return color_; }
    State state() const { return state_; }
    void set_rect(const SDL_Rect& rect) { rect_ = rect; }
    void set_state(State state) { state_ = state; }

    Node(const Node&) = delete;
    Node(Node&&) = delete;
    Node& operator=(const Node&) = delete;
    Node& operator=(Node&&) = delete;

private:
    virtual void self_update(Node* root);
    virtual void self_render(SDL_Renderer* renderer);

private:
    vector<Node*> subnodes_;
    string tag_;
    SDL_Rect rect_;
    SDL_Color color_;
    State state_;
};

Node::Node(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
    : tag_{tag}, rect_{rect}, color_{color}, state_{State::Active}
{}

Node::~Node()
{
    for (auto subnode : subnodes_) {
        delete subnode;
    }
}

void Node::update(Node* root)
{
    self_update(root);
    for (auto subnode : subnodes_) {
        subnode->update(root);
    }
}

void Node::render(SDL_Renderer* renderer)
{
    self_render(renderer);
    for (auto subnode : subnodes_) {
        subnode->render(renderer);
    }
}

void Node::on_collision_enter(Node* other) {}

void Node::add_subnode(Node* node)
{
    subnodes_.push_back(node);
}

Node* Node::find_subnode_by_tag(const string& tag)
{
    if (tag_ == tag) {
        return this;
    }

    for (auto n : subnodes_) {
        if (n->find_subnode_by_tag(tag) != nullptr) {
            return n;
        }
    }

    return nullptr;
}

void Node::kill_dead_subnodes()
{
    vector<Node*> dead_nodes;
    copy_if(subnodes_.begin(), subnodes_.end(),
            back_inserter(dead_nodes), is_dead);
    auto p = remove_if(subnodes_.begin(), subnodes_.end(), is_dead);
    subnodes_.erase(p, subnodes_.end());

    for (auto d : dead_nodes) {
        delete d;
    }

    for (auto n : subnodes_) {
        n->kill_dead_subnodes();
    }
}

void Node::self_update(Node* root) {}

void Node::self_render(SDL_Renderer* renderer)
{
    SDL_SetRenderDrawColor(renderer, color_.r, color_.g, color_.b, color_.a);
    SDL_RenderFillRect(renderer, &rect_);
}

class Paddle : public Node {
public:
    Paddle(const string& tag, const SDL_Rect& rect, const SDL_Color& color);
    ~Paddle();
    void set_move_scale(double move_scale) { move_scale_ = move_scale; }

private:
    void self_update(Node* root) override;

private:
    double move_scale_;
    const Uint8* key_state_;
};

Paddle::Paddle(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
    : Node{tag, rect, color}
    , move_scale_{0.0}
    , key_state_{SDL_GetKeyboardState(nullptr)}
{}

Paddle::~Paddle() {}

void Paddle::self_update(Node* root)
{
    double dx = 0.0;
    if (key_state_[SDL_SCANCODE_LEFT]) {
        dx -= 1.0;
    }
    if (key_state_[SDL_SCANCODE_RIGHT]) {
        dx += 1.0;
    }
    SDL_Rect r = rect();
    r.x += move_scale_ * dx;
    set_rect(r);

    resolve_collision(this, root);
}

class Ball : public Node {
public:
    Ball(const string& tag, const SDL_Rect& rect, const SDL_Color& color);
    ~Ball();
    void on_collision_enter(Node* other) override;
    Vec2 move_dir() const { return move_dir_; }
    void set_move_dir(const Vec2& move_dir) { move_dir_ = normalized(move_dir); }
    void set_move_scale(double move_scale) { move_scale_ = move_scale; }

private:
    void self_update(Node* root) override;

private:
    Vec2 move_dir_;
    double move_scale_;
};

Ball::Ball(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
    : Node{tag, rect, color}
    , move_dir_{0.0}
    , move_scale_{0.0}
{}

Ball::~Ball() {}

void Ball::on_collision_enter(Node* other)
{
    SDL_Rect prev;
    Vec2 move = scalar_multiplied(move_dir_, move_scale_);
    prev.x = round(rect().x - move.x);
    prev.y = round(rect().y - move.y);
    prev.w = rect().w;
    prev.h = rect().h;

    if (!overlaps_x(prev, other->rect())) {
        move_dir_.x *= -1;
    }

    if (!overlaps_y(prev, other->rect())) {
        move_dir_.y *= -1;
    }
}

void Ball::self_update(Node* root)
{
    Vec2 move = scalar_multiplied(move_dir_, move_scale_);
    SDL_Rect r = rect();
    r.x = round(rect().x + move.x);
    r.y = round(rect().y + move.y);
    set_rect(r);
}

class Brick : public Node {
public:
    Brick(const string& tag, const SDL_Rect& rect, const SDL_Color& color);
    ~Brick();
    void on_collision_enter(Node* other) override;

private:
    void self_update(Node* root) override;
};

Brick::Brick(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
    : Node{tag, rect, color}
{}

Brick::~Brick() {}

void Brick::self_update(Node* root)
{
    resolve_collision(this, root);
}

void Brick::on_collision_enter(Node* other)
{
    set_state(Node::State::Dead);
}

class Wall : public Node {
public:
    Wall(const string& tag, const SDL_Rect& rect, const SDL_Color& color);
    ~Wall();

private:
    void self_update(Node* root) override;
};

Wall::Wall(const string& tag, const SDL_Rect& rect, const SDL_Color& color)
    : Node{tag, rect, color}
{}

Wall::~Wall() {}

void Wall::self_update(Node* root)
{
    resolve_collision(this, root);
}

bool is_ball(const Node* node) { return node->tag() == "ball"; }
bool is_dead(const Node* node) { return node->state() == Node::State::Dead; }

Node* find_ball(Node* node)
{
    return node->find_subnode_by_tag("ball");
}

int left(const SDL_Rect& rect)   { return rect.x; }
int top(const SDL_Rect& rect)    { return rect.y; }
int right(const SDL_Rect& rect)  { return rect.x + rect.w - 1; }
int bottom(const SDL_Rect& rect) { return rect.y + rect.h - 1; }

bool overlaps_x(const SDL_Rect& a, const SDL_Rect& b)
{
    return !(left(a) > right(b) || right(a) < left(b));
}

bool overlaps_y(const SDL_Rect& a, const SDL_Rect& b)
{
    return !(top(a) > bottom(b) || bottom(a) < top(b));
}

void collision_entered(Node* a, Node* b)
{
    a->on_collision_enter(b);
    b->on_collision_enter(a);
}

void resolve_collision(Node* self, Node* root)
{
    auto ball = find_ball(root);
    if (ball == nullptr) {
        return; // not found
    }

    SDL_Rect rect1 = self->rect();
    SDL_Rect rect2 = ball->rect();
    if (SDL_HasIntersection(&rect1, &rect2)) {
        collision_entered(self, ball);
    }
}


void kill_dead_nodes(Node* node)
{
    if (is_dead(node)) {
        delete node;
        return;
    }
    node->kill_dead_subnodes();
}

void update(Node* root_node)
{
    root_node->update(root_node);
    kill_dead_nodes(root_node);
}

void render(SDL_Renderer* renderer, Node* root_node)
{
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    SDL_RenderClear(renderer);
    root_node->render(renderer);
    SDL_RenderPresent(renderer);
}

Node* setup_nodes()
{
    Node* root_node = new Node{"root", {}, {}};

    // temporary vars
    int w, h, x, y;
    SDL_Rect rect;
    SDL_Color color;

    // setup paddle
    w = 50;
    h = 10;
    x = ScreenWidth / 2 - w / 2; // horizontal center of screen
    y = ScreenHeight - 2 * h; // 1 unit above from bottom of screen
    rect = { x, y, w, h };
    color = { 222, 222, 255, 255 };
    auto paddle = new Paddle{"paddle", rect, color};
    paddle->set_move_scale(3.0);
    root_node->add_subnode(paddle);

    // setup ball
    w = 1.2 * paddle->rect().h;
    h = w; // square
    x = paddle->rect().x + (paddle->rect().w / 2) - (w / 2); // center of paddle
    y = paddle->rect().y - h; // upon paddle
    rect = { x, y, w, h };
    color = { 255, 128, 200, 255 };
    auto ball = new Ball{"ball", rect, color};
    ball->set_move_dir(Vec2{1.0, -1.0});
    ball->set_move_scale(3.0);
    root_node->add_subnode(ball);

    // setup bricks
    Node* bricks = new Node{"bricks", {}, {}};
    const int space = 4;
    const int margin = 50;
    const int columns = 10;
    const int rows = 15;
    const int brick_width =
        (ScreenWidth - (2 * margin) - ((columns - 1) * space)) / columns;
    const int brick_height = brick_width * 0.3;
    for (int y = 0; y < rows; ++y) {
        for (int x = 0; x < columns; ++x) {
            rect.x = x * (brick_width + space) + margin;
            rect.y = y * (brick_height + space) + margin;
            rect.w = brick_width;
            rect.h = brick_height;
            color.r = (columns - x) * (255 / columns);
            color.g = (y + 1) * (255 / rows);
            color.b = (x + 1) * (255 / columns);
            color.a = 255;
            auto brick = new Brick{"brick", rect, color};
            bricks->add_subnode(brick);
        }
    }
    root_node->add_subnode(bricks);

    // setup walls
    Node* walls = new Node{"walls", {}, {}};
    const int v_wall_width = 10;
    const int v_wall_height = ScreenHeight;
    const int h_wall_width = ScreenWidth - 2 * v_wall_width;
    const int h_wall_height = v_wall_width;
    color = {99, 38, 255, 255};
    rect = {v_wall_width, 0, h_wall_width, h_wall_height};
    auto top_wall = new Wall{"wall", rect, color};
    rect = {0, 0, v_wall_width, v_wall_height};
    auto left_wall = new Wall{"wall", rect, color};
    rect = {ScreenWidth - v_wall_width, 0, v_wall_width, v_wall_height};
    auto right_wall = new Wall{"wall", rect, color};
    walls->add_subnode(left_wall);
    walls->add_subnode(right_wall);
    walls->add_subnode(top_wall);
    root_node->add_subnode(walls);

    return root_node;
}

void main_loop(SDL_Renderer* renderer, Node* root_node)
{
    bool running = true;
    bool paused = true;
    while (running) {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            } else if (event.type == SDL_KEYDOWN) {
                switch (event.key.keysym.sym) {
                case SDLK_ESCAPE:
                case SDLK_RETURN:
                    running = false;
                    break;
                case SDLK_SPACE:
                    paused = !paused;
                    break;
                default:
                    break;
                }
            }
        }

        if (!paused) {
            update(root_node);
        }
        render(renderer, root_node);

        SDL_Delay(16);
    }
}

int main()
{
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        SDL_Log("SDL_Init() error: %s", SDL_GetError());
        exit(1);
    }

    SDL_Window* window =
        SDL_CreateWindow("Bricks SDL Version 5", 0, 0,
                         ScreenWidth, ScreenHeight, 0);
    if (window == nullptr) {
        SDL_Log("SDL_CreateWindow() error: %s", SDL_GetError());
        exit(1);
    }

    SDL_Renderer* renderer =
        SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
    if (renderer == nullptr) {
        SDL_Log("SDL_CreateRenderer() error: %s", SDL_GetError());
        exit(1);
    }

    Node* root_node = setup_nodes();

    main_loop(renderer, root_node);

    // cleanup
    delete root_node;
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
}