1

Software: Visual Studio 2017 Community

Hi, everybody,

I am making a simple 2d console game in C++ (perhaps very simplified Dwarf Fortress if you know it).

And I want a map to be displayed in console with ASCII.

Something like this:

enter image description here

I have a WorldMap class declared in the header file(simplified version). And I declare a 2d array inside it.

#pragma once
#include <iostream>

class WorldMap
{
public:
    WorldMap();
    virtual ~WorldMap();

private:
    int worldWidth;
    int worldHeight;
    char worldMap;     // Declare a variable that will hold all the characters for the map
};

And then define it in the .cpp file:

#include "WorldMap.h"
#include <algorithm>

WorldMap::WorldMap()
{
    worldWidth = 50;
    worldHeight = 50;
    worldMap[50][50];       // Define the map array
    // And then here I will also somehow need to be able to fill the whole map with '.' symbols, and so on
}

So that is the basic idea of what I am trying to achieve. The reason why I can't define the array size immediately is because I want to be able to choose the size of the map when the map is created.

I have already tried:

  1. The code above.

Error:

error C2109: subscript requires array or pointer type
  1. Declaring 2d array as char worldMap[][]; and then defining it as worldMap[50][50]; .

Error:

error C2087: 'worldMap': missing subscript
warning C4200: nonstandard extension used: zero-sized array in struct/union
message : This member will be ignored by a defaulted constructor or copy/move assignment operator
  1. Declaring 2d array as char worldMap[worldWidth][worldHeight];, expecting that when the object is created, the width and height variables will be defined first, and then they will define the array.

Error:

error C2327: 'WorldMap::worldWidth': is not a type name, static, or enumerator
error C2065: 'worldWidth': undeclared identifier
error C2327: 'WorldMap::worldHeight': is not a type name, static, or enumerator
error C2065: 'worldHeight': undeclared identifier
  1. Using char* worldMap; and char** worldMap, but so far I can't even understand how double pointer works, yet char* worldMap actually works with a 1D array without errors, until I start accessing values of the elements in the array.

I suppose a workaround would be to use a string or 1D char array and when displaying it just use mapWidth to end line each 50 characters for example, which will give the same result. But I feel like that's not a good way to achieve this since I will need to access x and y coords of this map and so on.

I guess what I am asking is:

  1. What's the best way of declaring a 2d array for a class and then defining it in the object?
  2. What's the best way to store a map for such a console game? (Not necessarily using arrays)

Thank you for reading. I will really appreciate any help, even just ideas and tips might push me in the right direction :)

  • 2
    For arrays whose size is not known at compile time you use dynamic memory allocation. In C++ you don't really need to do much, because the standard library gives you `std::vector` which does everything complex for you already. After getting to know this feature you might think that `std::vector>` might be what you are searching. This works, but performance wise it is not ideal. For the best performance people normally use lexic indexing (e.g. `idx = y * width + x`) which is pretty much what you were proposing yourself. This can then be wrapped into a class or function. – paleonix Apr 20 '21 at 19:21
  • 1
    @TedLyngmo Sorry I already deleted that comment, because that part could be interpreted as having an argument to the constructor, which might still be static. But I guess thats a bit far fetched... – paleonix Apr 20 '21 at 19:25
  • 1
    [Here's an example of a wrapper class](https://stackoverflow.com/a/2076668/4581301) that does what PaulG's talking about. – user4581301 Apr 20 '21 at 19:29
  • 1
    [This](https://stackoverflow.com/a/17260533/10107454) goes in-depth about the performance considerations I mentioned. – paleonix Apr 20 '21 at 19:29
  • 1
    Finally the reasons why your tries didn't work: 1. `WorldMap` is a `char` not an array of chars. This would never work. 2. The `char array[num_elements]` declaration only works in Standard C/C++ if `num_elements` is known at compile time. There are some compilers (gcc) which allow you to use it differently (maybe even like you tried, I don't know), that is meant by "nonstandard extension". 3. Starting from a pointer is a step into the direction of dynamic memory, but you still need to allocate memory for it to work. But using a "raw" pointer like that is the C way of doing things. – paleonix Apr 20 '21 at 19:42
  • If you're going with a 1d array you may want to implement `operator[]` for accessing the elements as 2d array: `std::unique_prt m_elements; /* should contain an array of w * h elements */ char* operator[](size_t y) { return m_elements.get() + (w * y); }` which would allow you to do `myObject[y][x]` to access an element – fabian Apr 20 '21 at 20:52
  • You should add an extra column to the end. All the values in this extra column should be `'\n'`. The last slot in the board should be `'\0'`. This allows you to print the board with: `std::cout << worldMap << std::endl;`. – Thomas Matthews Apr 20 '21 at 21:44

2 Answers2

1
  1. What's the best way of declaring a 2d array for a class and then defining it in the object?
  2. What's the best way to store a map for such a console game? (Not necessarily using arrays)

This is not "the best way" but it's one way of doing it.

  • Create a class wrapping a 1D std::vector<char>.
  • Add operator()s to access the individual elements.
  • Add misc. other support functions to the class, like save() and restore().

I've used your class as a base and tried to document what it's doing in the code: If some of the functions I've used are unfamiliar, I recommend looking them up at https://en.cppreference.com/ which is an excellent wiki that often has examples of how to use the particular function you read about.

#include <algorithm>   // std::copy, std::copy_n
#include <filesystem>  // std::filesystem::path
#include <fstream>     // std::ifstream, std::ofstream
#include <iostream>    // std::cin, std::cout
#include <iterator>    // std::ostreambuf_iterator, std::istreambuf_iterator
#include <vector>      // std::vector

class WorldMap {
public:
    WorldMap(unsigned h = 5, unsigned w = 5) : // colon starts the initializer list
        worldHeight(h),      // initialize worldHeight with the value in h
        worldWidth(w),       // initialize worldWidth with the value in w
        worldMap(h * w, '.') // initialize the vector, size h*w and filled with dots.
    {}

    // Don't make the destructor virtual unless you use polymorphism
    // In fact, you should probably not create a user-defined destructor at all for this.
    //virtual ~WorldMap(); // removed

    unsigned getHeight() const { return worldHeight; }
    unsigned getWidth() const { return worldWidth; }

    // Define operators to give both const and non-const access to the
    // positions in the map.
    char operator()(unsigned y, unsigned x) const { return worldMap[y*worldWidth + x]; }
    char& operator()(unsigned y, unsigned x) { return worldMap[y*worldWidth + x]; }

    // A function to print the map on screen - or to some other ostream if that's needed
    void print(std::ostream& os = std::cout) const {
        for(unsigned y = 0; y < getHeight(); ++y) {
            for(unsigned x = 0; x < getWidth(); ++x)
                os << (*this)(y, x); // dereference "this" to call the const operator()
            os << '\n';
        }
        os << '\n';
    }

    // functions to save and restore the map
    std::ostream& save(std::ostream& os) const {
        os << worldHeight << '\n' << worldWidth << '\n'; // save the dimensions

        // copy the map out to the stream
        std::copy(worldMap.begin(), worldMap.end(), 
                  std::ostreambuf_iterator<char>(os));
        return os;
    }

    std::istream& restore(std::istream& is) {
        is >> worldHeight >> worldWidth;            // read the dimensions
        is.ignore(2, '\n');                         // ignore the newline
        worldMap.clear();                           // empty the map
        worldMap.reserve(worldHeight * worldWidth); // reserve space for the new map

        // copy the map from the stream
        std::copy_n(std::istreambuf_iterator<char>(is),
                    worldHeight * worldWidth, std::back_inserter(worldMap));
        return is;
    }

    // functions to save/restore using a filename
    bool save(const std::filesystem::path& filename) const {
        if(std::ofstream ofs(filename); ofs) {
            return static_cast<bool>(save(ofs)); // true if it suceeded
        }
        return false;
    }

    bool restore(const std::filesystem::path& filename) {
        if(std::ifstream ifs(filename); ifs) {
            return static_cast<bool>(restore(ifs)); // true if it succeeded
        }
        return false;
    }

private:
    unsigned worldHeight;
    unsigned worldWidth;

    // Declare a variable that will hold all the characters for the map
    std::vector<char> worldMap;
};

Demo

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • Wow! Thank you a lot! This looks like a very professionally written code to me, at least considering I am a beginner. It's quite complicated but very clear. Took me a few good hours to analyze it all and I learned a lot from this. Already implemented modified version of this in my code. So this was super helpful :) The only thing I have not yet found information about is ```(*this)(y,x)```. Specifically, why is ```*this``` wrapped in brackets? Maybe this way of using brackets has a unique name? – Yamiko Hikari Apr 21 '21 at 10:35
  • @YamikoHikari I'm glad it helped and you're welcome! Instead of `(*this)(y,x)` one could write `operator()(y, x);` - it's just a way to access the user defined `operator()`(s). The brackets is because of [operator precedence](https://en.cppreference.com/w/cpp/language/operator_precedence). Without the brackets it'd be the same as `*(this(y,x))` (trying to call the `this` pointer as a function (and then dereference the result), not the `WorldMap&` that `*this` results in) – Ted Lyngmo Apr 21 '21 at 10:42
0

There is no best way to do anything*. It's what works best for you.

From what I understand you want to make a dynamic 2D arrays to hold your char of world map. You have a lot of options to do this. You can have a worldMap class nothing wrong with that. If you want dynamic 2D arrays just make functions out of this kind of logic.

#include <iostream>
#include <vector>

int main() {
    int H = 10, W = 20;
    char** map = NULL; //This would go in your class.H

    //Make a function to allocate 2D array 
    map = new char* [H];
    for (int i = 0; i < H; i++) {
        map[i] = new char[W];
    }
    //FILL WITH WHATEVER 
    for (int i = 0; i < H; i++) {
        for (int j = 0; j < W; j++) {
            map[i][j] = 'A';
        }
    }
    //do what ever you want like normal 2d array 
    for (int i = 0; i < H; i++) {
        for (int j = 0; j < W; j++) {
            std::cout << map[i][j] << " ";
        }
        std::cout << std::endl;
    }
    //Should always delete when or if you want to make a new one run time  
    for (int i = 0; i < H; i++)    
        delete[] map[i];
    delete[] map;              
    map = NULL;

    //Also you can use vectors
    std::cout << "\n\n With vector " << std::endl;
    std::vector<std::vector<char>> mapV; 

    //FILL WITH WHATEVER 
    for (int i = 0; i < H; i++) {
        std::vector<char> inner;
        for (int j = 0; j < W; j++) {
            inner.push_back('V');
        }
        mapV.push_back(inner);
    }
    //do what ever you want kind of like a normal array 
    //but you should look up how they really work 
    for (int i = 0; i < H; i++) {
        for (int j = 0; j < W; j++) {
            std::cout << mapV[i][j] << " ";
        }
        std::cout << std::endl;
    }

    mapV.clear();

    return 0;
}

boardkeystown
  • 180
  • 1
  • 11