silverua / slay-the-spire-map-in-unity

Implementation of the Slay the Spire Map in Unity3d
MIT License
273 stars 65 forks source link

Shuffling Extension (I know, not your code) returning Index out of range. #32

Open jmeyer1980 opened 3 months ago

jmeyer1980 commented 3 months ago

In MapPlayerTracker I have Set up my nodes like this:

        {
            // we have access to blueprint name here as well
            Debug.Log("Entering node: " + mapNode.Node.blueprintName + " of type: " + mapNode.Node.nodeType);
            // load appropriate scene with context based on nodeType:
            // or show appropriate GUI over the map: 
            // if you choose to show GUI in some of these cases, do not forget to set "Locked" in MapPlayerTracker back to false
            switch (mapNode.Node.nodeType)
            {
            case NodeType.CommonEnemy:
                // Load the scene or enable the UI game object for CommonEnemy
                if (!string.IsNullOrEmpty(mapNode.Blueprint.sceneName))
                    SceneManager.LoadScene(mapNode.Blueprint.sceneName);
                else if (mapNode.Blueprint.uiObject != null)
                    mapNode.Blueprint.uiObject.SetActive(true);
                break;
            case NodeType.MinorEnemy:
                // Load the scene or enable the UI game object for MinorEnemy
                if (!string.IsNullOrEmpty(mapNode.Blueprint.sceneName))
                    SceneManager.LoadScene(mapNode.Blueprint.sceneName);
                else if (mapNode.Blueprint.uiObject != null)
                    mapNode.Blueprint.uiObject.SetActive(true);
                    break;

MapGenerator:

public static class MapGenerator
    {
        private static MapConfig config;

        private static readonly List<NodeType> RandomNodes = new List<NodeType>
        {   
            NodeType.CommonEnemy,
            NodeType.MinorEnemy,
            NodeType.EliteEnemy,
            NodeType.RestSite,
            NodeType.Treasure,
            NodeType.Store,
            NodeType.Mystery,
        };

NodeBlueprint:

namespace Map
{
    public enum NodeType
    {
        CommonEnemy,
        MinorEnemy,
        EliteEnemy,
        RestSite,
        Treasure,
        Store,
        Boss,
        Mystery
    }
}

For some reason, I continue to receive the error. Am I missing something?

ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
System.Collections.Generic.List`1[T].get_Item (System.Int32 index) (at <dc753a1061284f8e971ee88ee4826eee>:0)
Map.ShufflingExtension.Random[T] (System.Collections.Generic.IList`1[T] list) (at Assets/STSMap/Scripts/ShufflingExtension.cs:29)
Map.MapGenerator.PlaceLayer (System.Int32 layerIndex) (at Assets/STSMap/Scripts/MapGenerator.cs:84)
Map.MapGenerator.GetMap (Map.MapConfig conf) (at Assets/STSMap/Scripts/MapGenerator.cs:41)
Map.MapManager.GenerateNewMap () (at Assets/STSMap/Scripts/MapManager.cs:41)
UnityEngine.Events.InvokableCall.Invoke () (at <6f7018b8b8c149e68c4a65a05ac289be>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <6f7018b8b8c149e68c4a65a05ac289be>:0)
UnityEngine.UI.Button.Press () (at ./Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:70)
UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at ./Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:114)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at ./Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:57)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at ./Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at ./Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:530)
silverua commented 3 months ago

I think, the line where you get the error must be this one: var blueprintName = config.nodeBlueprints.Where(b => b.nodeType == nodeType).ToList().Random().name; Have you created the NodeBlueprint scriptable objects for CommonEnemy, MinorEnemy and have you added those scriptable objects into the nodeBlueprints list in your MapConfig scriptable object?

jmeyer1980 commented 3 months ago

Minor enemy is a part of the default nodes that came with the zip I downloaded from here. The only node "type" I added and created is CommonEnemy. The nodes that belong to the types common and minor are the even numbered tiers and odd numbered tiers respectively. I honestly thought I covered everything. image

jmeyer1980 commented 3 months ago

I know I have to be missing something because this happens every time I try to add any additional node types. For my game, I T10-T0 are difficulty level scenes and I am trying to launch those scenes using the different node types. I think I am explaining correctly. When I use the default node types, I can launch A scene without issue. I am not good enough with scriptable objects to easily change which enemies spawn based on a difficulty curve... but I am going to figure it out eventually. But I need to learn how to successfully add new node types first.

silverua commented 3 months ago

Do you have any node blueprints of type EliteEnemy in Default Map Config / Node Blueprints? Asking because you have the EliteEnemy node type and it's featured in Random Nodes. It might throw errors if the randomization lands on EliteEnemy for the random node and the config does not have any EliteEnemy node bluepritnts.

A quick test that may help with this - you can try setting all the Randomize Nodes sliders to 0 and seeing if it eliminates the errors.

jmeyer1980 commented 3 months ago

I have errors from Oneline as well. I only mention this because I can't adjust the randomize node values in the default map config? image

jmeyer1980 commented 3 months ago

To the best of my knowledge, the only things I changed were the code in the three scripts to add the scene/gui loading stuff and to add the ONE node type: CommonEnemy.

Edit: I gotta take a nap. Might be a while before I can reply again.

silverua commented 3 months ago

Have a good nap! Something to try later: I'd try breaking it down further and checking what the logs say before you get this error. For example:

var nodeType = Random.Range(0f, 1f) < layer.randomizeNodes ? GetRandomNode() : layer.nodeType;
var list = config.nodeBlueprints.Where(b => b.nodeType == nodeType).ToList();
Debug.Log($"{list.Count} nodes of type {nodeType} found in config");
var blueprintName = list.Random().name;

This way before the error happens, you would get a log that hints which type of node was causing an issue and how many of them you have in your MapCofig. It it says, 0 nodes, you have to fix that by making and adding a NodeBlueprint of that type into your MapConfig.

jmeyer1980 commented 3 months ago

Wasn't able to get back to it today. Will try again tomorrow.

jmeyer1980 commented 3 months ago

I'm sorry, I have autism and ADHD. If what I am doing isn't working and I feel like I am doing it correctly, I get tunnel vision and can't see what I am missing. I have to keep trying over and over until whatever I am missing clicks.

I reimported the files and made a video where I retrace my steps and reproduce the error. It is eight minutes because it takes me a few to remember what I am doing again. lol. I am praying that you notice what I am missing. I couldn't figure out exactly where I needed to add that debug line, so that hasn't been tried yet.

https://youtu.be/_OBtwaMvfpI

silverua commented 3 months ago

Thanks for sending the vid. Critical things that I am seeing:

So, if you want to do this without introducing any errors, you do it in this order:

  1. You add an enum entry for CommonEnemy in code in the end of the enum. Adding in the end is very important.
  2. You create a Node Blueprint object for Common Enemy. It's okay to do it the way you did by copying Minor Enemy or any other existing Node Blueprint.
  3. Also very important: right after you create the Common Enemy node blueprint, you select it and in the Inspector set the Node Type to Common Enemy enum value that you have created in step 1.
  4. Then you go to your map config and add the Common Enemy node blueprint to the list.
  5. Then you adjust the node Types in layers whichever way you want.

And it should work. Adding new NodeTypes into MapGenerator.RandomNodes is optional and not super important. You can do it later.

silverua commented 3 months ago

As for the code that I've mentioned above, I think in your case adding it is a good idea because if you forget to do some bits of the config, it will remind you and steer you in the right direction.

You can replace this line in MapGenerator.PlaceLayer:

var blueprintName = config.nodeBlueprints.Where(b => b.nodeType == nodeType).ToList().Random().name;

With these lines:

var nodeType = Random.Range(0f, 1f) < layer.randomizeNodes ? GetRandomNode() : layer.nodeType;
var list = config.nodeBlueprints.Where(b => b.nodeType == nodeType).ToList();
Debug.Log($"{list.Count} nodes of type {nodeType} found in config");
var blueprintName = list.Random().name;

Then if you ever get an error you will be warned about which NodeType caused it.

jmeyer1980 commented 3 months ago

So I was doing it backwards, taking stuff from where it belonged and rearranging it when I shouldn't have. What if I wanted to scrap them all and re-add the nodes I want in the order I want? Would Unity accept that? Or is it just best that I leave the current things where they are cause it aint broke and adding new ones will just work if I continue following your instructions?

I wish I could say that I am new to all of this, but I've been putzing around with Unity since 2017. My biggest issue is that I don't know enough C# to write code from scratch. I am utilizing Bing's new notepad feature for AI generated code. I could have used it to try and figure this out... but the couple of times I tried, it told me I was doing it right. lol. That's when I came back here again.

I also have the Single Player CCG kit. I was able to use it's map generator as a stand-alone feature, but it doesn't have the UI based version like yours does and when I add it to my project, the boss location is shifted too far out of the camera view no matter what I try. So I use yours. Hopefully, I am able to continue adding things to the end of the enums here and get things working. If I understood things better... I could use a single level scene and have a difficulty curve set up the scene for me. For now, I will use the multiple scenes method. yuck. lol.

BTW. Thank you.

silverua commented 3 months ago

Glad that you made some progress!

You can add enums in the order that you prefer, but then literally everywhere in the Unity project you have to go and set them manually. Which is: on all the Node Blueprint objects and in the Map Config object.

If you ever want to add more enum entries, remember that adding anywhere besides the end of the enum messes up the setup that you did in Unity and forces you to re-check everything again.

That's why the least intrusive way is to add new entries to the end of the list.

AI is still not great for coding games unfortunately. I would not fully rely on it. If you need to write code for your game, I think, it's still a great idea to learn how to code yourself and not rely on AI too much. It's making a lot of mistakes and often suggests solutions that are just wrong.

jmeyer1980 commented 3 months ago

I have learned more in the last six months than I have in the last five years and that is all thanks to AI. I understand the limitations and the risks, but I would never have gotten as far as I have with this shmup than without it. It just works for my learning disability.

I do my own code when I can and when I can recall what I need for the occasion and that is happening way more often than it used to. So I will respectfully continue to use AI as a crutch until I don't need to any more.

Right now, what I am unsure of, as I am writing this, is how I am going to handle victories. Like how to return to the map and start act 2. I'm sure I have the right idea: set victory bool, load map scene, confirm victory bool, trigger map gen while referencing the Act2 config, and then (I think) lock the nodes to force the player to click the first one?

silverua commented 3 months ago

By all means! Whatever works for you!

So, when you win, while you are in a different scene, you can do something like:

PlayerPrefs.DeleteKey("Map");
PlayerPrefs.Save();

This ensures that:

Another thing that you can do before going into the map scene is set the act number, so you could do something like:

PlayerPrefs.SetInt("ActIndex", 1);

Then you load the map scene.

And in the map scene you can have a new script that would set the MapConfig based on the value in PlayerPrefs. For example:

public class ActSetter : MonoBehaviour
{
    public MapManager mapManager;
    public MapConfig[] acts;

    private void Awake()
    {
        var index = PlayerPrefs.HasKey("ActIndex") ? PlayerPrefs.GetInt("ActIndex") : 0;
        if (index < 0 || index >= acts.Length) index = 0;
        mapManager.config = acts[index];
    }
}

Add it to an object in the Map scene, link the MapManager and populate the acts array in the inspector.

And when you lose the game, you can also delete the map, but set the act to 0:

PlayerPrefs.DeleteKey("Map");
PlayerPrefs.Save();
PlayerPrefs.SetInt("ActIndex", 0);

Hope it helps.

jmeyer1980 commented 3 months ago

I don't know if it will let me comment on a closed topic, but here we go: Would you mind explaining to me how to unlock the next nodes after victory on the current scene? I have tried using SetAttainableNodes() but it seems to want an object reference? Which I assume are the next nodes on our path?

Edit: cool. It let me re-open. I felt this was better than creating a new topic since we already covered new act and failed attempt.

silverua commented 3 months ago

Personally, I think, it is better to create new issues if we're discussing a different topic. It makes it easier for people in a similar situation to find answers to their questions. But we can carry on here for now.

By design the method SetAttainableNodes() should be called automatically when the player moves or when the map is loaded. It is based on the path that player has covered on the map so far. So, unlocking next nodes should happen automatically. If it is not happening, something must be going wrong and we should investigate why it's happening.

Can you describe what you're doing and what the expectation is:

jmeyer1980 commented 3 months ago

Personally, I think, it is better to create new issues if we're discussing a different topic. It makes it easier for people in a similar situation to find answers to their questions. But we can carry on here for now.

By design the method SetAttainableNodes() should be called automatically when the player moves or when the map is loaded. It is based on the path that player has covered on the map so far. So, unlocking next nodes should happen automatically. If it is not happening, something must be going wrong and we should investigate why it's happening.

Can you describe what you're doing and what the expectation is:

  • is the gameplay in a different scene or is it in the same scene?
  • when you're returning to the map, are you loading the map scene or are you re-enabling the object with the map?
  • do you get any errors in the console?
  • when you return to the map, what's happening exactly? Is the player in the correct position, but the next nodes are not highlighted? Something else?

This was a pre-emptive question. I was still setting up my win/loss UI and the question was weighing on my mind. Thank you.

Here's the most recent vid I took of what I have done so far. It's not much, but it is something. https://youtu.be/Bw_DYrVZP6Y

silverua commented 3 months ago

That's looking pretty good! Congrats! Keep it up!

jmeyer1980 commented 3 months ago

That's looking pretty good! Congrats! Keep it up!

Thank you very much. The ship selection screen was actually a lot of fun to work on. Getting the selected ship through the map screen and into the level was also, sarcastically, fun. When it finally worked, though, I scared my dog. lol. I literally jumped out of my seat.