2D RogueLike structure

Hello,

I am currently learning to develop on Unity through a game on my own. I decided to do some kind of Rogue like game with rooms and door quite exactly like Binding of Isaac.

I managed to get something but i’m not sure about my solution and need you guys point of view :

  1. So i have one player and one “Level Generator” script in my scene. My script generator generate all the room of a level. For the player to go to another room, it’ll walk through the door and then i’ll change position of my player and camera.
    In other words, i do not have 1scene/1 room but more 1scene/1level and my camera moving around rooms following my player.

  2. I have a prefab Room with some walls and doors and ennemies in it. My levelGenerator instantiate as many rooms as needed. I do not instantiate door/walls/ennemies, they just are created since the parenting with the room. Should i do it in another way ?

That’s basically the point at the moment, I also have a lot of connections between my elements (my enemies have a Room and a Player attribute), my Player has a Room attribute etc… but i guess it’s necessary.

Anyway, not really a problem here, just want some advices about how i could do better or what shouldn’t I do.

Thanks,
Jules

Personally I’d recommend doing the enemies and the room generation in different scripts. What you are doing isn’t wrong, however it will create a more cluttered environment to work in. Spreading them out into singular purpose scripts will allow changes to be made easier (as you know exactly were it is) as well bugs will be easier to find.

1 Like

Indeed, that’s smarter.

Thanks !

Personally, this is how I would go about it (never played Binding of Isaac btw):

Dissociate the visual display from the data. You could have maps that hold a dictionary<Vector2Int, int>. You create a reference table and you bind ints to their respective sprites.

At runtime, you should have a model class that contains a property for the map currently loaded. You pass it a map and then the model is responsible for letting everyone who’s interested know that it’s changed its loaded map.

Something along the lines of :

public static EventHandler ActiveMapUpdatedEventHandler;

public Map ActiveMap { get; private set; }

public void SetMap(Map activeMap)
{
    ActiveMap = activeMap;
    //This will notify interested parties that the active map has changed
    ActiveMapUpdatedEventHandler?.Invoke(this, EventArgs.Empty);
}

Then, you just hook stuff to it.

void OnEnable()
{
    MapModel.ActiveMapUpdatedEventHandler += OnActiveMapUpdated;
}

void OnDisable()
{
    MapModel.ActiveMapUpdatedEventHandler -= OnActiveMapUpdated;
}

void OnActiveMapUpdated(object sender, System.EventArgs e)
{
    MapModel mapModel = sender as MapModel;
    //You can then access the map model to get the goodies
    Dictionary<Vector2Int, int> mapLayout = mapModel.ActiveMap.mapLayout;
    //Create a visual display of the map based on the layout...
}

This creates a clean break between the data and how it’s displayed. It’ll make your life easier, I promise.

Ideally, you should see a clear distinction between the data, its operating logic and its visual display. Your architecture should reflect this as much as possible.

1 Like

First of all, thank you for the detailled answer :slight_smile:

Let me recap to see if i got this right.
I’ll :

  • 1 database table referencing all the sprites i need for every room.
  • 1 generating level script
  • 1 managingRooms script. One of his attribute is the current created map. He’s in charge of managing rooms and querying the database to get the sprites for the next room.
  • 1 room script that instanciate everything
  • and many scripts for each gameobject instantiated.

Am i good ? :slight_smile:

I still have a question about background (walls, doors) btw. Should i instantiate sprites and relatively position them or should i have some prefabs Rooms with background sprites manually placed ?

Thank you

Not quite. The rooms manager is absolutely not in charge of getting the sprites. It is simply a data container and the only logic it contains is broadcasting events to let subscribers know when its data is modified.

how it works

This might be a bit tricky to grasp initially. To simplify things, pretend we have class A, B and C. For the sake of making this a bit more concrete, pretend we’re trying to display a timer on the screen that goes from 60 to 0.

Class A is a data container and nothing else. It contains the timer variable and has no logic that operates on its data. The only logic it contains is broadcasting events to let interested classes know when its data has been modified.

Class B contains operating logic. Its purpose is to modify the data of Class A. In this case, it could be a coroutine that brings down Class A timer from 60 to 0 over 60 seconds.

Class C’s purpose is to give the user a visual representation of the data (the timer on the screen). In this case, it could be strapped to a gameobject that contains a UI text component. Class C will hook onto Class A’s event to be notified when its data is modified.

What will happen is Class B will progressively change the values of Class A. Class A will broadcast an event when its data is modified. Class C will be notified and will display the timer accordingly.

In this case, Class C will modify its text component to reflect the timer value and it could also animate the transform a bit to give it life.

The example above is obviously overkill for that scenario but should give a good idea of how the pattern works.

For you, you were talking about prefabs, so I’m guessing your rooms aren’t procedurally generated. It’s really hard for me to help you with a concrete implementation without knowing exactly how your rooms/maps are built.

Ideally, you would create a “Room” or “Map” container class/file that would contain the initial position of every game elements in the map and all the relevant goodies. How you do this is up to you. You should have as many instances as you have maps/rooms.

Then, you should have a controller class that’s responsible for handling the flow of the game. It passes whatever map you want to load to the model class (that’s Class A in my explanation) when you want to load a new map/room.

When you do that, the model will notify everyone who’s interested that the map has changed.

Remember Class C in my example who was responsible for displaying the timer? You need the equivalent of that. We’ll call it MapView. MapView hooks onto the model and as soon as the map changes, its responsible for clearing the old map on the screen and displaying the new one.

It does that by reading the data from the model to figure out what goes where and then recreates the map. So, to answer that last question of yours, your map probably shouldn’t be a big prefab, and yes, you should instantiate the sprites. Better yet, pool a bunch of objects with sprite renderers and just assign the right sprites.

Furthermore, you could hook an additional view for your enemies. Hook it to your model and when the map changes, its responsible for reading the data and clearing/displaying your enemies. etc.

That’s the cool thing about this pattern, the model(data container) doesn’t have to be aware of the existence of the views. You can hook/delete as many views as you want and you’ll never run into a coupling problem. You just create/hook a bunch of stuff based on the visual information you want to provide to your user, as long as the model holds the proper data (that could be the map, the enemies, the character, anything you want, really!).

You can get really deep with this stuff, or use it simply for the map, or not use it at all!

If you’re not snoring yet, consider that this may also not be the best implementation for your use-case. It’s only one implementation that has its pros and cons.

Hope that helped more than it brought confusion.

1 Like

Thank you very much for your answer, i might need few days to fully understand and try to reproduce what you are explaining right there but that’s very cool !

I’ll definitely give it a chance and come back to you when i’ll be done :slight_smile:

If this feels a bit overwhelming, there’s no shame in trying a different approach that you feel more suitable mate. In all cases, please don’t feel like you absolutely need to try it and that I am expecting an answer.

However, if you do and you happen to run into problems along the way, I’ll be happy to help.

Best of luck with your project!

I found these articles immensely helpful:

http://journal.stuffwithstuff.com/category/roguelike/

I read and implemented this one in Unity and it generates great dungeon levels. I simplified the hall generation a lot because my dungeon is first-person and all those twisties were annoying:

Rooms and Mazes: A Procedural Dungeon Generator

I also took a lot away from this one, to do with timing and taking turns:

A Turn-Based Game Loop

Alright ! Thank you so much @ADNCG , after few hours i managed to have something looking right ! I had to changes a few things from what you told me in order to make it works but now it’s okay.

I’ll show you what i did :

One Room.class that only got a type for the moment :

public class Room{
    private int roomType;

    public Room(int type) {
        roomType = type;
    }
    public int getRoomType() { return roomType; }
}

RoomManager :

    public delegate void RoomEventHandler(Room room,String comingFrom);
    public static event RoomEventHandler newRoom;

    public Room activeRoom { get; private set; }


    public void SetMap(Room room, String comingFrom) {
        //This will notify interested parties that the active map has changed
            if (newRoom != null) {
                newRoom(room, comingFrom);
                Debug.Log("event sent");
            }
    }

As you see, since i’m using prefabs rooms for the moment (let’s not make it too hard already :smile:) i’m giving as parameters a room with a type (maybe i could only pass the type and create the room in the RoomView script ?)

GameManager :

public class GameManager : MonoBehaviour {
   
    public RoomManager roomManager;
    private RoomView roomView;
   
    void Start () {
        roomManager = new RoomManager();
        if (roomView == null) {
            roomView = gameObject.AddComponent<RoomView>();
        }

        Room room1 = new Room(0);
        roomManager.SetMap(room1,"");
    }

    //Events
    void OnEnable() {
        DoorTrigger.passingDoor += ChangingRoom;
    }
    void OnDisable() {
        DoorTrigger.passingDoor += ChangingRoom;
    }
    void ChangingRoom(string comingFrom) {
        Room room1 = new Room(1);
        roomManager.SetMap(room1, comingFrom);
    }
}

And finally the RoomView :

    private Transform player;

    private void Start() {
        player = GameObject.FindGameObjectWithTag("Player").GetComponent<Transform>();
    }

    void OnEnable() {
        RoomManager.newRoom += OnActiveMapUpdated;
    }

    void OnDisable() {
        RoomManager.newRoom -= OnActiveMapUpdated;
    }

    void OnActiveMapUpdated(Room room, string comingFrom) {
        Debug.Log("room changed : " + room);
        if(room.getRoomType() == 0) {
            GameObject createdRoom = (GameObject)Instantiate(Resources.Load("Prefabs/StartingRoom"));
        }
        else if(room.getRoomType() == 1) {
            GameObject createdRoom = (GameObject)Instantiate(Resources.Load("Prefabs/Room"));
        }

        if ("North".Equals(comingFrom)) {
            player.transform.position = new Vector2(0, -4);
        }
    }

with a bit of unrelative scripts about triggers sending event to change room and replace the player.

Anyway, i tested it and it works like a charm ! Thank you very much for your advices it’s indeed much more cleaner than what I was doing in a first place.

@Kurt-Dekker Thank you for the interesting links, I’ll rethink the way i wanna generate the rooms with them, the generating algorithm I took is quite boring at the moment :slight_smile:

You definitely got the idea now.

Your controller(GameManager) should be responsible for handling the logic relative to the game flow, that includes generating rooms.

Remember, whenever you can, you want to make a distinction between the data, the elements manipulating the data and the data’s visual representation. The view is the visual representation and the only logic it should carry is logic associated to visually representing the data. Segregating these 3 elements as much as possible will help with maintainability.

Accessing pooled objects that carried nothing but a sprite renderer component was one thing. Being in charge of generating a room that contains a bunch of enemies is an entire other thing. That’s game logic right there and shouldn’t be in the hands of the view.

Here, @RavenOfCode 's comment definitely applies. Because of your prefab structure, the generation of the room and the enemies are indissociable. Same goes for, say, the generation of the room and the visual representation of the room generated. It makes it impossible for 2 different parties to handle the 2 very different tasks.

In the current state, the pattern doesn’t serve its purpose and you’re probably better off without it. I’d hang on a bit longer to it though. You might end up redesigning the room generation, like you mentioned, and it just might come in handy :slight_smile:

Secondly, if you decide to use that pattern, you might want to rename the RoomManager to something more general that will support all the data relative to the core game (don’t get me wrong, if it gets pretty big, you can organize it into a data structure that makes sense).

Keep in mind that, in the current state, what you’re doing is you’re limiting the access to the data by passing specific values to subscribers. That’s ok! In the long term, if your model ends up supporting a bunch of data, you might want to change that.

It’s just an example but let’s say your game had lives, kind of like those games from the 90’s had. When changing stages, maybe you would want to have a view that displayed the amount of lives remaining for a short amount of time. If, upon being notified that the stage has changed, the only data the view could access was the value of the new stage (like in your room manager, where you pass the room as a parameter), then it wouldn’t be able to display the amount of lives left.

This is why it can be a good thing to pass a reference to the model as a parameter, instead, and let the views be responsible for accessing the data they need.

I hope this is making sense.

1 Like

It is making sense, not fully yet though :slight_smile:

I got few questions :

  • My RoomView should not use “Instantiate(…)” and leave it to the RoomManager (that will get a rename :wink: ) and just be used to position the objects ? Or should it get a list of Object and instantiate them itself (for example walls and doors, if i stop using prefabsRooms)?
  • Every logic stuff coming from the Room such a counting the enemy left before opening doors should be done in the GameManager one ? I thought he’d be the one handling other things like music/changing scenes etc

I’ll work on separating the enemy and room generating scripts and pass a dictionnary<Vector2,int> instead of a room parameters as you recommanded in the first place.

Honestly, I may just have been complicating stuff needlessly rather than helping you out properly. Let’s try again but more to the point.

I just watched a video of Binding of Isaac. As far as I could tell, things from the environment are either : Can be moved on / can’t be moved on but destroyable / can’t be moved on and undestroyable. Bosses and enemies seem to have colliders with sizes that are inconsistent with the rest of the game’s environment(for good reasons). Some things are animated, some aren’t. Even holes?

All the game elements don’t have that much in common. For that reason, I recommend creating a prefab for every single element in the game. Individual prefabs rather than your big prefab.

Once that is done, you could create a class that holds a Dictionary<int, GameObject> and map all the prefabs to an index, or store it in a text file that you will read from, or whatever floats your boat. As long as you have a way to say that prefab x = integer y.

Next step would be to find out if you want to use procedural generation or not. The rest will depend heavily on what you choose here. To me, it seems like most of the fun comes from the fact that it’s procedurally generated.

That’s a more complex topic but @Kurt-Dekker is always right on point. That’ll definitely be more help than I can provide with this.

Once you’re done, it’s really just reading the maze’s data and instantiating the matching stuff.

1 Like

Complicating is sometime one step to learning so it’s ok, I wanna get something as clean as I can. :slight_smile:

I dropped the big prefab and am indeed going to prefab each type of Object.

but I still don’t understand why should i get a dictionnary of the prefabs (or a textfile). What is the point of doing this and not just do
(GameObject)Instantiate(Resources.Load(“MyPrefab”)) instead ?
I can pass some lists of GameObject and have my RoomView to instantiate and place them for me.

I’m going to do some more research and comparing between procedurally and randomly for the future.

As usual, thank you very much :slight_smile:

When you read up on maze generation, it will make more sense.

The maze generation and the instantiation of all the gameobjects composing the game are 2 different things. I’ll try to illustrate the concept in an easy to read way.

void GenerateRoom()
{
    Dictionary<Vector2Int, int> room = new Dictionary<Vector2Int, int>();
    //Room will be 4 units large
    int roomWidth = 4;
    //Room will be 5 units tall
    int roomHeight = 5;

    for (int x = 0; x < roomWidth; x++)
    {
        for (int y = 0; y < roomHeight; y++)
        {
            //The current position represented by the current x and current y
            Vector2Int position = new Vector2Int(x, y);

            //Checks against this bool to know if the cell should be a wall
            if (IsAWall(position, roomWidth, roomHeight))
            {
                //Its a wall, we're giving it the value 1
                room.Add(position, 1);
            }
            else
            {
                //It's not a wall, we're going to give it 0
                room.Add(position, 0);
            }
        }
    }
}

bool IsAWall(Vector2Int position, int roomWidth, int roomHeight)
{
    //If the position is located on the west wall, its a wall
    if (position.x == 0)
        return true;
    //If position is located on the east wall, its a wall
    if (position.x == roomWidth - 1)
        return true;
    //If position is located on the south wall, its a wall
    if (position.y == 0)
        return true;
    //If position is located on the north wall, its a wall
    if (position.y == roomHeight - 1)
        return true;

    return false;
}

If you were to draw the results on a sheet, it would look like this 3599380--292100--room.png
Now in practice, the maze generation is a lot more complex as there is a lot more to consider, but the principle is the same in the way that it will output the maze in a format that you can read and create a visual representation out of.

In that dictionary that I mentioned in the previous post, the one where you are indexing your prefabs, you would have the wall gameobject represented by 1 and the ground gameobject represented by 0.

Then, if you wanted a visual representation of the maze, you would iterate through all the coordinates, get the matching gameobject from the dictionary and instantiate it.

Thank you very much for the clear explanation. It makes more sense now (i’m a slow learner, thank you for your patience btw haha).

I have one final question about the instantiating. Who is in charge of instantiating ?

I’d say the RoomView script as you explained earlier but i’m not sure about it.

We’re all learning together.

Just go with what you are comfortable with and try to remain consistent. Forget my earlier posts, just keep in mind that separating the data from the logic is a good idea and then build around that.

For me, it’d be RoomView since all the rooms in Binding of Isaac seem to be the same, at least from what I saw in the video. The only thing that changes is the theme, he then seems to put the doors directly over the walls and the holes directly over the ground (without actually changing the walls/ground or whatever).

The room itself never really changes, it just changes theme and is completely static, as far as I could tell.

After that, the dynamic content is added and probably should be treated in a different way than the room itself.

Since there’s no game logic involved in the room itself, for me it would make sense that it would be managed by a class that simply reads the current theme upon changing room and then updates its sprites to match it.

Yep, i’ll try to keep that in mind as much as i can.

Well you definitely gave my structure a bit of sense, even if I did not manage to understand apply all of your points, it seems not that bad after the bit of work i did today.

Thank you very much.