Putting the 'role' back in role-playing games since 2002.
Donate to Codex
Good Old Games
  • Welcome to rpgcodex.net, a site dedicated to discussing computer based role-playing games in a free and open fashion. We're less strict than other forums, but please refer to the rules.

    "This message is awaiting moderator approval": All new users must pass through our moderation queue before they will be able to post normally. Until your account has "passed" your posts will only be visible to yourself (and moderators) until they are approved. Give us a week to get around to approving / deleting / ignoring your mundane opinion on crap before hassling us about it. Once you have passed the moderation period (think of it as a test), you will be able to post normally, just like all the other retards.

Vapourware Codexian Game Development Thread

Joined
Jul 26, 2015
Messages
1,361
PC RPG Website of the Year, 2015 Codex 2016 - The Age of Grimoire Make the Codex Great Again! Grab the Codex by the pussy Insert Title Here Divinity: Original Sin 2 BattleTech Bubbles In Memoria A Beautifully Desolate Campaign Pillars of Eternity 2: Deadfire Pathfinder: Kingmaker Steve gets a Kidney but I don't even get a tag. My team has the sexiest and deadliest waifus you can recruit.
Made this game called "The Last Shuttle" as a capstone project for my school's program.

https://professaur-chris.itch.io/the-last-shuttle


This was made in Unreal as a C++ and Blueprints project (VR and PC). I was a level-designer/generalist on the project.

My end tasks included; Designing the level layout (2D) and iterations that needed to be made based on feedback and relaying that to the world builder, creating the intended player flow (e.g. challenges, obstacles, interactions, etc.), Making and creating sequences within the game, creating concept art for the levels, Implementing sound into the game as well as designing sounds for the game, creating scripts for the game to use around the levels, as well as testing and fixing bugs.

This took approximately 13 weeks to make. We had a team of around 18 students of varying skill levels.

Originally I was just designing the levels, but was later brought in to help speed things along about mid-way through the project due to production not going as smoothly and quickly as we should have based on our milestones/deadlines.
 
Last edited:

CryptRat

Arcane
Developer
Joined
Sep 10, 2014
Messages
3,625
What's the licence for that? Making middle length fantasy Note of the ouskirts is probably not something which would take me a lot of time, and there's absolutely no way I could find better assets than these ones.
 

zwanzig_zwoelf

Graverobber Foundation
Developer
Joined
Nov 21, 2015
Messages
3,178
Location
デゼニランド
What's the licence for that? Making middle length fantasy Note of the ouskirts is probably not something which would take me a lot of time, and there's absolutely no way I could find better assets than these ones.
Check the page. You can use these assets in your games, but certain assets can't be used in commercial games -- you can find the list in the description.
 
Joined
Dec 24, 2018
Messages
1,899
Displaying names of the empires on then occupied territories. Almost good. Phew, it is very hard to solve.
Finally I have found the method used in Europa Universalis dev's reddit post

In case it's useful to you, here is a function I made that generates labels based off of their method. It's not perfect, there are a few edge cases that aren't great, but by and large it works well.

Code:
void Map_Shape::generate_curved_labels(const std::string& p_label_text, float p_scale)
{
    constexpr int SPARSE_FACTOR{4};
    constexpr int OFFSET_COUNT{5};
    constexpr int LENGTH_STEP{2};
    constexpr int MINIMUM_LABEL_LENGTH{10};
    constexpr int SPLINE_POINT_COUNT{4};

    m_labels.clear();
    for (const auto& path: m_border_paths)
    {
        if (path.size() < 3 || clockwise_polygon(path))
        { continue; }

        auto&& [x_min, x_max] = std::minmax_element(path.begin(), path.end(),
                                                    [](const auto& lhs, const auto& rhs)
                                                    { return lhs.x < rhs.x; });
        auto&& [y_min, y_max] = std::minmax_element(path.begin(), path.end(),
                                                    [](const auto& lhs, const auto& rhs)
                                                    { return lhs.y < rhs.y; });

        auto min_x = static_cast<int>(x_min->x);
        auto min_y = static_cast<int>(y_min->y);
        auto max_x = static_cast<int>(x_max->x);
        auto max_y = static_cast<int>(y_max->y);

        std::vector<Clipper2Lib::Point64> contained_points;

        for (int x{min_x}; x < max_x; x += SPARSE_FACTOR)
        {
            for (int y{min_y}; y < max_y; y += SPARSE_FACTOR)
            {
                Clipper2Lib::Point64 point{x, y};
                if (point_in_poly(path, point))
                { contained_points.push_back(point); }
            }
        }

        double slope                  = y_slope(contained_points);
        double main_intercept         = y_intercept(contained_points, slope);
        double intercept_offset_scale = std::abs(max_y - min_y) * 0.8;

        double longest_length{0.0};
        double intercept = main_intercept - (intercept_offset_scale / 2.0);

        Clipper2Lib::Point64 main_line_start{0, 0};
        Clipper2Lib::Point64 main_line_stop{0, 0};

        for (int i{0}; i < OFFSET_COUNT; ++i)
        {
            Clipper2Lib::Point64 start{0, 0};
            Clipper2Lib::Point64 stop{0, 0};

            double length = std::abs(measure_line_ymxb(path, slope, intercept,
                                                       min_x, max_x,
                                                       start, stop, LENGTH_STEP));
            if (length > longest_length)
            {
                longest_length  = length;
                main_line_start = start;
                main_line_stop  = stop;
            }
            intercept += intercept_offset_scale / static_cast<double>(OFFSET_COUNT);
        }

        if (longest_length < MINIMUM_LABEL_LENGTH)
        { continue; }

        std::array<Clipper2Lib::Point64, SPLINE_POINT_COUNT> offsets;

        double spline_point{0.0};
        double spline_increment{1.0 / static_cast<double>(SPLINE_POINT_COUNT)};

        for (std::size_t i{0}; i < SPLINE_POINT_COUNT; ++i)
        {
            offsets[i] = get_point_on_line(main_line_start, slope, longest_length * spline_point);
            spline_point += spline_increment;
        }

        auto perpendicular_slope = -(1.0 / slope);

        std::array<Clipper2Lib::Point64, SPLINE_POINT_COUNT> base_points;

        for (std::size_t i{0}; i < SPLINE_POINT_COUNT; ++i)
        {
            base_points[i] = get_point_on_line(offsets[i], perpendicular_slope, -longest_length);
        }

        std::array<double, SPLINE_POINT_COUNT> intercepts{0.0};

        for (std::size_t i{0}; i < SPLINE_POINT_COUNT; ++i)
        {
            intercepts[i] = static_cast<double>(base_points[i].y) -
                            perpendicular_slope * static_cast<double>(base_points[i].x);
        }

        std::array<Clipper2Lib::Point64, SPLINE_POINT_COUNT> start_points;
        std::array<Clipper2Lib::Point64, SPLINE_POINT_COUNT> stop_points;

        for (std::size_t i{0}; i < SPLINE_POINT_COUNT; ++i)
        {
            if (measure_line_ymxb(path, perpendicular_slope, intercepts[i], min_x, max_x,
                                  start_points[i], stop_points[i], LENGTH_STEP) < 1.0)
            {
                start_points[i] = offsets[i];
                stop_points[i]  = offsets[i];
            }
        }

        std::vector<glm::vec2> spline_points;

        for (std::size_t i{0}; i < SPLINE_POINT_COUNT; ++i)
        {
            spline_points.emplace_back(((static_cast<float>(start_points[i].x + stop_points[i].x) / 2.0f) +
                                        static_cast<float>(offsets[i].x)) / 2.0f,
                                       ((static_cast<float>(start_points[i].y + stop_points[i].y) / 2.0f) +
                                        static_cast<float>(offsets[i].y)) / 2.0f);
        }

        auto  lx    = static_cast<float>(main_line_start.x);
        auto  ly    = static_cast<float>(main_line_start.y);
        auto  ll    = static_cast<float>(longest_length * 0.8);
        float angle = (std::atan(static_cast<float>(slope)) * 180.0f / static_cast<float>(std::numbers::pi));
        Label l{p_label_text, lx, ly, ll, angle, "cantarell", spline_points};
        m_labels.push_back(l);
    }
}

m_border_paths is a vector of vectors of x, y coordinates that define a shape's border, onto which a label will be placed.
spline_points is used for a label's internal UniformCubicBSpline<glm::vec2> curve, which is from SplineLibrary.

And a couple functions that are used in that one (as separate functions mainly for readability).

Code:
double y_slope(const std::vector<Clipper2Lib::Point64>& p_points)
{
    auto N  = static_cast<double>(p_points.size());
    auto x  = static_cast<double>(std::accumulate(p_points.begin(), p_points.end(), 0ll,
                                                  [](const auto& lhs, const auto& rhs)
                                                  { return lhs + rhs.x; }));
    auto y  = static_cast<double>(std::accumulate(p_points.begin(), p_points.end(), 0ll,
                                                  [&](const auto& lhs, const auto& rhs)
                                                  { return lhs + rhs.y; }));
    auto x2 = static_cast<double>(std::accumulate(p_points.begin(), p_points.end(), 0ll,
                                                  [&](const auto& lhs, const auto& rhs)
                                                  { return lhs + (rhs.x * rhs.x); }));
    auto xy = static_cast<double>(std::accumulate(p_points.begin(), p_points.end(), 0ll,
                                                  [&](const auto& lhs, const auto& rhs)
                                                  { return lhs + (rhs.x * rhs.y); }));

    return ((N * xy) - (x * y)) / ((N * x2) - (x * x));
}

double y_intercept(const std::vector<Clipper2Lib::Point64>& p_points, double p_slope)
{
    auto N = static_cast<double>(p_points.size());
    auto x = static_cast<double>(std::accumulate(p_points.begin(), p_points.end(), 0ll,
                                                 [](const auto& lhs, const auto& rhs)
                                                 { return lhs + rhs.x; }));
    auto y = static_cast<double>(std::accumulate(p_points.begin(), p_points.end(), 0ll,
                                                 [&](const auto& lhs, const auto& rhs)
                                                 { return lhs + rhs.y; }));
    return (y - (p_slope * x)) / N;
}

double y_slope_intercept(double p_slope, double p_x, double p_y_intercept)
{
    return (p_slope * p_x) + p_y_intercept;
}

double measure_line_ymxb(const std::vector<Clipper2Lib::Point64>& p_points, double p_slope, double p_intercept,
                         int p_first_x, int p_last_x, Clipper2Lib::Point64& p_start, Clipper2Lib::Point64& p_stop,
                         int p_step)
{
    double length{0.0};
    Clipper2Lib::Point64 last_point = {p_first_x,
                                          static_cast<int>(y_slope_intercept(p_slope, p_first_x, p_intercept))};
    bool started_line{false};
    for(int x = p_first_x; x < p_last_x; x += p_step)
    {
        Clipper2Lib::Point64 point = {x, static_cast<int>(y_slope_intercept(p_slope, x, p_intercept))};
        if (!started_line && point_in_poly(p_points, point))
        {
            p_start = point;
            p_stop = point;
            started_line = true;
        }
        if (started_line && !point_in_poly(p_points, point))
        {
            p_stop = last_point;
            break;
        }
        last_point = point;
    }

    if (started_line)
    { length = std::sqrt(std::pow(p_stop.x - p_start.x, 2) + (std::pow(p_stop.y - p_start.y, 2))); }

    return length;
}

Clipper2Lib::Point64 get_point_on_line(Clipper2Lib::Point64 p_base, double p_slope, double p_length)
{
    Clipper2Lib::Point64 point{0, 0};

    double delta_x = p_length / std::sqrt(1 + (p_slope * p_slope));
    double delta_y = p_slope * delta_x;

    point.x = p_base.x + static_cast<int>(delta_x);
    point.y = p_base.y + static_cast<int>(delta_y);

    return point;
}

The curve generated is used like so:

Code:
void World_Map::register_label(const std::string& p_layer, Label& p_label)
{
    Object_Registration registration;

    float total_advance{0.0f};
    float max_height{0.0f};
    float advance{0.0f};
    float scale{p_label.span / static_cast<float>(p_label.text.length())};

    for (std::string::const_iterator c = p_label.text.begin(); c != p_label.text.end(); ++c)
    {
        auto char_index = static_cast<int>(static_cast<unsigned char>(*c));
        auto& character = m_atlases[p_label.atlas].characters[char_index];
        max_height = std::max(max_height, character.bottom - character.top);
        total_advance += character.advance * scale;
    }

    for (std::string::const_iterator c = p_label.text.begin(); c != p_label.text.end(); ++c)
    {
        auto char_index = static_cast<int>(static_cast<unsigned char>(*c));
        auto& character = m_atlases[p_label.atlas].characters[char_index];

        registration.mesh.name = std::string{p_label.atlas + std::to_string(char_index)};

        float interpolation_start   = (advance / total_advance) * p_label.curve.totalLength();
        float interpolation_stop    = (((advance + (character.advance * scale)) / total_advance) *
                                       p_label.curve.totalLength());
        float interpolation_average = (interpolation_start + interpolation_stop) / 2.0f;

        glm::vec2 xy_start = p_label.curve.getPosition(interpolation_start);
        glm::vec2 xy_stop  = p_label.curve.getPosition(interpolation_stop);
        glm::vec2 xy_coord = p_label.curve.getPosition(interpolation_average);

        float angle = std::atan2((xy_stop.y - xy_start.y), (xy_stop.x - xy_start.x));

        glm::mat4 model{1.0f};
        model = glm::translate(model, glm::vec3{xy_start.x, xy_start.y + max_height, 0.0f});
        model = glm::scale(model, glm::vec3{scale, scale, 1.0f});
        model = glm::rotate(model, angle, glm::vec3{0.0f, 0.0f, 1.0f});

        registration.transform               = model;
        registration.object.transform_matrix = model;
        registration.object.render_boundary  = generate_boundary(registration.mesh);

        std::optional<std::size_t> handle = l_renderer.register_cartographic_object(p_layer, registration);

        if (handle.has_value())
        { p_label.render_handles.push_back(handle.value()); }

        advance += (character.advance * scale);
    }
}

Exact usage depends on how you render text, of course, but that shows basically how you would use a SplineLibrary curve generated by the initial generate_curved_labels function to place characters one at a time.

It took me a while to figure out exactly what the Paradox developer meant with each stage of his post, so perhaps a code example will save you some time.
 
Last edited:

Justinian

Arcane
Developer
Joined
Oct 21, 2022
Messages
292
Dammit it's been like a month since i did any real work (beyond fucking around with the controller). This can't go on. Gonna spend the rest of the day shooting footage of my game, cutting it up, posting it on youtube, and maybe writing a couple of promotional posts on reddit.

Should probably upload my game to itch.io as well. Anyone using gamemaker have experience with it? Do you just upload the installable Gamemaker generates? With steam I had to actually install it on my system then steam packages it up. I don't have any DRM so that should greatly simplify the process.
 

TheDeveloperDude

MagicScreen Games
Developer
Joined
Jan 9, 2012
Messages
614
One of the hardest thing is balancing the game. I suspect that is why they are using the level-scaling cheap trick.
Anyway it is nice when I find a surprising thing, like this 66 XP army. That means they have won 66 battles so far.
 

d1nolore

Savant
Joined
May 31, 2017
Messages
721
One of the hardest thing is balancing the game. I suspect that is why they are using the level-scaling cheap trick.
Anyway it is nice when I find a surprising thing, like this 66 XP army. That means they have won 66 battles so far.

This is starting to look much better, good job.
 

Twiglard

Poland Stronk
Patron
Staff Member
Joined
Aug 6, 2014
Messages
7,509
Location
Poland
Strap Yourselves In Codex Year of the Donut
I'm working on lighting. It's difficult.

Somebody should make an indie game like Myth: TFL. With a game engine a prototype would take less than a week.
 

As an Amazon Associate, rpgcodex.net earns from qualifying purchases.
Back
Top Bottom