Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Voting for the Unity Awards are OPEN! We’re looking to celebrate creators across games, industry, film, and many more categories. Cast your vote now for all categories
    Dismiss Notice
  3. Dismiss Notice

2D RogueLike structure

Discussion in 'Scripting' started by JulesCvl, Aug 14, 2018.

  1. JulesCvl

    JulesCvl

    Joined:
    Dec 30, 2016
    Posts:
    11
    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
     
  2. RavenOfCode

    RavenOfCode

    Joined:
    Apr 5, 2015
    Posts:
    869
    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.
     
    Doug_B likes this.
  3. JulesCvl

    JulesCvl

    Joined:
    Dec 30, 2016
    Posts:
    11
    Indeed, that's smarter.

    Thanks !
     
  4. ADNCG

    ADNCG

    Joined:
    Jun 9, 2014
    Posts:
    990
    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 :
    Code (CSharp):
    1. public static EventHandler ActiveMapUpdatedEventHandler;
    2.  
    3. public Map ActiveMap { get; private set; }
    4.  
    5. public void SetMap(Map activeMap)
    6. {
    7.     ActiveMap = activeMap;
    8.     //This will notify interested parties that the active map has changed
    9.     ActiveMapUpdatedEventHandler?.Invoke(this, EventArgs.Empty);
    10. }
    Then, you just hook stuff to it.
    Code (CSharp):
    1. void OnEnable()
    2. {
    3.     MapModel.ActiveMapUpdatedEventHandler += OnActiveMapUpdated;
    4. }
    5.  
    6. void OnDisable()
    7. {
    8.     MapModel.ActiveMapUpdatedEventHandler -= OnActiveMapUpdated;
    9. }
    10.  
    11. void OnActiveMapUpdated(object sender, System.EventArgs e)
    12. {
    13.     MapModel mapModel = sender as MapModel;
    14.     //You can then access the map model to get the goodies
    15.     Dictionary<Vector2Int, int> mapLayout = mapModel.ActiveMap.mapLayout;
    16.     //Create a visual display of the map based on the layout...
    17. }
    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.
     
    RavenOfCode likes this.
  5. JulesCvl

    JulesCvl

    Joined:
    Dec 30, 2016
    Posts:
    11
    First of all, thank you for the detailled answer :)

    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 ? :)

    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
     
  6. ADNCG

    ADNCG

    Joined:
    Jun 9, 2014
    Posts:
    990
    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.

    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.
     
    Last edited: Aug 15, 2018
    RavenOfCode likes this.
  7. JulesCvl

    JulesCvl

    Joined:
    Dec 30, 2016
    Posts:
    11
    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 :)
     
  8. ADNCG

    ADNCG

    Joined:
    Jun 9, 2014
    Posts:
    990
    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!
     
  9. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,599
  10. JulesCvl

    JulesCvl

    Joined:
    Dec 30, 2016
    Posts:
    11
    Alright ! Thank you so much @AntoineDesbiens, 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 :
    Code (CSharp):
    1. public class Room{
    2.     private int roomType;
    3.  
    4.     public Room(int type) {
    5.         roomType = type;
    6.     }
    7.     public int getRoomType() { return roomType; }
    8. }
    9.  
    RoomManager :
    Code (CSharp):
    1.     public delegate void RoomEventHandler(Room room,String comingFrom);
    2.     public static event RoomEventHandler newRoom;
    3.  
    4.     public Room activeRoom { get; private set; }
    5.  
    6.  
    7.     public void SetMap(Room room, String comingFrom) {
    8.         //This will notify interested parties that the active map has changed
    9.             if (newRoom != null) {
    10.                 newRoom(room, comingFrom);
    11.                 Debug.Log("event sent");
    12.             }
    13.     }
    As you see, since i'm using prefabs rooms for the moment (let's not make it too hard already :D) 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 :
    Code (CSharp):
    1. public class GameManager : MonoBehaviour {
    2.    
    3.     public RoomManager roomManager;
    4.     private RoomView roomView;
    5.    
    6.     void Start () {
    7.         roomManager = new RoomManager();
    8.         if (roomView == null) {
    9.             roomView = gameObject.AddComponent<RoomView>();
    10.         }
    11.  
    12.         Room room1 = new Room(0);
    13.         roomManager.SetMap(room1,"");
    14.     }
    15.  
    16.     //Events
    17.     void OnEnable() {
    18.         DoorTrigger.passingDoor += ChangingRoom;
    19.     }
    20.     void OnDisable() {
    21.         DoorTrigger.passingDoor += ChangingRoom;
    22.     }
    23.     void ChangingRoom(string comingFrom) {
    24.         Room room1 = new Room(1);
    25.         roomManager.SetMap(room1, comingFrom);
    26.     }
    27. }

    And finally the RoomView :
    Code (CSharp):
    1.  
    2.     private Transform player;
    3.  
    4.     private void Start() {
    5.         player = GameObject.FindGameObjectWithTag("Player").GetComponent<Transform>();
    6.     }
    7.  
    8.     void OnEnable() {
    9.         RoomManager.newRoom += OnActiveMapUpdated;
    10.     }
    11.  
    12.     void OnDisable() {
    13.         RoomManager.newRoom -= OnActiveMapUpdated;
    14.     }
    15.  
    16.     void OnActiveMapUpdated(Room room, string comingFrom) {
    17.         Debug.Log("room changed : " + room);
    18.         if(room.getRoomType() == 0) {
    19.             GameObject createdRoom = (GameObject)Instantiate(Resources.Load("Prefabs/StartingRoom"));
    20.         }
    21.         else if(room.getRoomType() == 1) {
    22.             GameObject createdRoom = (GameObject)Instantiate(Resources.Load("Prefabs/Room"));
    23.         }
    24.  
    25.         if ("North".Equals(comingFrom)) {
    26.             player.transform.position = new Vector2(0, -4);
    27.         }
    28.     }
    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 :)
     
  11. ADNCG

    ADNCG

    Joined:
    Jun 9, 2014
    Posts:
    990
    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 :)


    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.
     
    Last edited: Aug 15, 2018
    RavenOfCode likes this.
  12. JulesCvl

    JulesCvl

    Joined:
    Dec 30, 2016
    Posts:
    11
    It is making sense, not fully yet though :)

    I got few questions :
    - My RoomView should not use "Instantiate(..)" and leave it to the RoomManager (that will get a rename ;) ) 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.
     
    Last edited: Aug 15, 2018
  13. ADNCG

    ADNCG

    Joined:
    Jun 9, 2014
    Posts:
    990
    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.
     
    Last edited: Aug 16, 2018
    Kurt-Dekker likes this.
  14. JulesCvl

    JulesCvl

    Joined:
    Dec 30, 2016
    Posts:
    11
    Complicating is sometime one step to learning so it's ok, I wanna get something as clean as I can. :)

    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 :)
     
  15. ADNCG

    ADNCG

    Joined:
    Jun 9, 2014
    Posts:
    990
    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.
    Code (CSharp):
    1.  
    2. void GenerateRoom()
    3. {
    4.     Dictionary<Vector2Int, int> room = new Dictionary<Vector2Int, int>();
    5.     //Room will be 4 units large
    6.     int roomWidth = 4;
    7.     //Room will be 5 units tall
    8.     int roomHeight = 5;
    9.  
    10.     for (int x = 0; x < roomWidth; x++)
    11.     {
    12.         for (int y = 0; y < roomHeight; y++)
    13.         {
    14.             //The current position represented by the current x and current y
    15.             Vector2Int position = new Vector2Int(x, y);
    16.  
    17.             //Checks against this bool to know if the cell should be a wall
    18.             if (IsAWall(position, roomWidth, roomHeight))
    19.             {
    20.                 //Its a wall, we're giving it the value 1
    21.                 room.Add(position, 1);
    22.             }
    23.             else
    24.             {
    25.                 //It's not a wall, we're going to give it 0
    26.                 room.Add(position, 0);
    27.             }
    28.         }
    29.     }
    30. }
    31.  
    32. bool IsAWall(Vector2Int position, int roomWidth, int roomHeight)
    33. {
    34.     //If the position is located on the west wall, its a wall
    35.     if (position.x == 0)
    36.         return true;
    37.     //If position is located on the east wall, its a wall
    38.     if (position.x == roomWidth - 1)
    39.         return true;
    40.     //If position is located on the south wall, its a wall
    41.     if (position.y == 0)
    42.         return true;
    43.     //If position is located on the north wall, its a wall
    44.     if (position.y == roomHeight - 1)
    45.         return true;
    46.  
    47.     return false;
    48. }
    49.  
    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.
     
    Last edited: Aug 16, 2018
  16. JulesCvl

    JulesCvl

    Joined:
    Dec 30, 2016
    Posts:
    11
    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.
     
  17. ADNCG

    ADNCG

    Joined:
    Jun 9, 2014
    Posts:
    990
    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.
     
    Last edited: Aug 16, 2018
  18. JulesCvl

    JulesCvl

    Joined:
    Dec 30, 2016
    Posts:
    11
    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.