Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Civ-like notifications with custom actions upon click

Discussion in 'Scripting' started by c-Row, Sep 23, 2021.

  1. c-Row

    c-Row

    Joined:
    Nov 10, 2009
    Posts:
    847
    In my current game I would like to add notifications as seen in games like Civilization or Age Of Wonders, and while the basic UI setup is pretty straightforward I am wondering how I can implement it so that a left-click on a certain type of notification will trigger a specific action?

    For example, a city having finished building a unit or expansion will probably center the camera on the location, while the "research finished" notification opens the research screen, a declaration of war would lead to the diplomacy screen and so on...

    How would you try and solve this? Different button prefabs based on the type of notification? A bare-bones button which gets a single component attached to it at runtime that listens to the button's onClick event? Something completely different?
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,514
    I think this is where I would start. These notifications are often called "toasts" because they pop up, and a generic way of making a toast is to have an interface to it that accepts varying parameters such as :

    - a text description
    - an graphical icon
    - a time it stays visible before auto-disappearing
    - an optional function to call if you tap on the toast before it auto-disappears
    - an sound it plays on appearance and disappearance

    You might want a toast manager so in case three or four of these things fire at the same time, it can be smart and not stick them all on top of each other. Some solutions might be to stack them vertically in a layout, or else show them in sequence and wait until they dismiss to show the next one.
     
    c-Row and Lethn like this.
  3. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    In Civilization, the notifications don't go away until the user interacts with them, you can often have more of them than even fit on the screen, and the notification list is part of a specific screen, rather than being an "overlay"--other things can appear on top of notifications, and the notifications persist if you leave the screen and then come back. I'd describe that more as a "log" than as "toasts", though I suppose a lot of the same implementation guidelines apply.

    (In fact, I think Civ would benefit from having some sort of "notification history" screen, where you can see notifications that you've already dismissed or from previous turns. But that's probably beyond scope for this thread.)

    You can modify a Button at runtime to call any function you want, using Button.onClick.AddListener. This will only directly allow you to add a function with no arguments, but you can easily get around this by wrapping your function call in a lambda expression. For instance, if you want the button to call MyFunction(7, 42), you could write

    Code (CSharp):
    1. button.onClick.AddListener( () => MyFunction(7, 42) );
    Warning: If your lambda expression contains a variable, it saves a reference to that variable, not its current value! This often trips people up when they want to create a bunch of lambda in a loop; e.g.
    Code (CSharp):
    1. for (int i = 0; i < 10; ++i)
    2. {
    3.     myButtons[i].onClick.AddListener(()=>MyFunction(i));
    4. }
    5. // COMMON MISTAKE -
    6. // the above code will cause every button to call MyFunction(10),
    7. // because by the time they get CLICKED, i == 10
    8.  
    9. // instead, do this:
    10. for (int i = 0; i < 10; ++i)
    11. {
    12.     int temp = i;
    13.     myButtons[i].onClick.AddListener(()=>MyFunction(temp));
    14. }
    15. // This uses a separate variable (scoped to just that loop iteration)
    16. // for each button
    So, one approach is to have the code that creates the notification also figure out exactly what function should be called (with what parameters) when it gets clicked, and then add that as a listener to the button.

    But you also might want to think about putting some of the intelligence "after" the click, rather than before. For instance, if you have a notification saying that your production queue is empty in the city of Rome, then instead of making the button call OpenProductionQueue(Rome), you might want to have it call ClickedOnNotification(EmptyQueue, Rome). This way, you can decide to make clicking on the notification do different things based on the situation at the time it's clicked, instead of only at the time it's created.

    (Also, you might want the notification object itself to be one of the parameters you pass to the function--then the function can decide to do things like delete the notification, if you want.)



    EDIT: Oh! You also might want to take a few minutes before you start to think about how you are going to serialize your notifications--if your game is similar to Civilization, then you probably want them to persist if the user saves the game and then loads it tomorrow! (Endless Legend/Endless Space fail to save their notifications, and it drives me crazy.)

    This will push you more towards having all notifications call something like ClickedOnNotifcation(someNotificationVariable) and then read all necessary data out of the notification object, because serializing anonymous functions is somewhat problematic.
     
    Last edited: Sep 23, 2021
    c-Row and Kurt-Dekker like this.
  4. c-Row

    c-Row

    Joined:
    Nov 10, 2009
    Posts:
    847
    That's pretty much what I want to achieve, yes. :)

    The one thing I wanted to prevent was to have a single notification prefab including all possible types of reactions, but then again that's just personal taste. Having them all wrapped up in their own functions to be called in the lambda expression sounds like the most efficient way to handle that, though.

    I already have an event system in place so that sounds like a good idea as well. :)

    Which brings us back to the 'notification history' feature you pointed out at the top of your reply. I feel the best approach would be to have a NotificationManager every sender can send a new message to for it to decide what kind of notification it should spawn, whether or not it has been reacted to, and so on. When loading the game it could then check the list and select all entries for which a notification is needed but has not been reacted to.

    Some notifications should probably be filtered from the list as default, e.g. combat notifications ("Unit X has slain enemy Unit Y") since those might be of no interest at a later point.

    (I would also like to bring up Stellaris as a bad example of games desperately in need of a message log...)