New object created from dictionary modifies original in dictionary

I’m having a tough time figuring out what I’m doing wrong here so any help is appreciated. I’m a self-taught dev who works alone, so please be easy on me :wink:

Background
I’m trying to create a Character system for my little RPG project. The goal is to have Character “templates” which I can build in the editor, then instance and modify in-game.

I’m using a lot of scriptable objects in my Resources folder, which are attached to a game object called CharacterDB. At runtime, the CharacterDB script creates new instances of the scriptable objects, then creates new non-Monobehaviour Character objects at runtime.

In the CharacterDB, I have a method called GetCharacter, which takes a GUID, and should return a new copy of the Character. For one in-game use, I created a CharacterPrefab class, which I want to call the Character data from CharacterDB, then overwrite parts of it when I call an Initialize method. Example use case would be changing the name of a character from “Monster001” to "

Problem
When I change the name in CharacterPrefab, it’s changing the name of the source data stored in the dictionary in CharacterDB. Anyhow, here’s my code:

CharacterDB

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CharacterDB : MonoBehaviour
{
    [SerializeField] private List<CharacterSO> _characterSOList;
    private Dictionary<string, Character> _charactersDict = new Dictionary<string, Character>();
    private ItemDB _itemDB;

    private void Awake(){
        _itemDB = FindObjectOfType<ItemDB>();
        if (_itemDB == null){
            Debug.Log("Cannot locate ItemDB.");
        }
    }

    private void Start(){       
        foreach (CharacterSO characterSO in _characterSOList){
            CharacterSO newCharacterSO = Instantiate(characterSO);
            bool isValid = ValidateData(characterSO);
            if (!isValid){
                throw new System.Exception($"{newCharacterSO} has invalid item data. Fix and restart.");
            }
            else{
                List<Item> items = new List<Item>();
                foreach (string id in newCharacterSO.StartingItemGUIDs){
                    Item item = _itemDB.GetItem(id);
                    items.Add(item);
                }
                Character character = new Character(newCharacterSO.Portrait, newCharacterSO.AttributeSheet, newCharacterSO.InventoryCapacity, items);
                _charactersDict.Add(newCharacterSO.ID, character);
            }
        }
       
    }

    private bool ValidateData(CharacterSO characterSO){
        bool isValid = true;
        foreach (string id in characterSO.StartingItemGUIDs){
            isValid = _itemDB.ItemsDict.ContainsKey(id);

            if (!isValid){
                isValid = false;
                break;
            }
        }
        return isValid;
    }

    public Character GetCharacter(string id){ // Issue here???
        if (_charactersDict.ContainsKey(id)){
            Character character = new Character(_charactersDict[id].PortraitSprite, _charactersDict[id].AttributeSheet, _charactersDict[id].InventoryCapacity, _charactersDict[id].Items);
            return character;
        }
        else{
            return null;
        }
    }

    [ContextMenu("Print DB Contents")]
    private void PrintDBContents(){
        foreach (var entry in _charactersDict){
            Debug.Log($"{entry.Key}, {entry.Value.AttributeSheet.Name}");
        }
    }
}

CharacterPrefab

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CharacterPrefab : MonoBehaviour
{
    [SerializeField] private string _name;
    [SerializeField] private string _characterTemplateID;
    [SerializeField] private Alignment _alignment;
    private Character _character;
    private CharacterDB _characterDB;

    public Character Character { get { return _character; } }

    private void Start(){
        InitializeCharacter(_alignment);
    }

    public void InitializeCharacter(Alignment alignment){
        _characterDB = FindObjectOfType<CharacterDB>();
        if (_characterDB == null){
            Debug.Log("Cannot find CharacterDB");
        }

        _character = _characterDB.GetCharacter(_characterTemplateID);

        // Set character alignment
        if (_alignment == Alignment.Player){
            _character.CombatantType = Alignment.Player;
        }
        else if (_alignment == Alignment.Enemy){
            _character.CombatantType = Alignment.Enemy;
        }

        // Initialize health
        _character.Health = _character.AttributeSheet.MaxHP;

        // Set in-game name
        if (_name != null){
            _character.Name = _name;
        }
        this.name = $"{_character.CombatantType}: {_character.Name}";
    }

    [ContextMenu("Debug Character")]
    private void DebugCharacter(){
        foreach (Item item in this.Character.Items){
            if (this.Character.Items.Count <= 0){
                Debug.Log($"{this} has no Items.");
            }
            else{
                Debug.Log($"Character prefab {this} has in Items: {item.ItemName} which isEquipped: {item.IsEquipped}");
            }
        }
    }
}

I’m guessing that Character must be a class, which means that it is a reference type.

Simply assigning it to another variable doesn’t make a copy the way it might with an integer for instance.

It just makes another reference to same original object.

If you want a fresh copy, you have to make a new one explicitly.

Value Types vs Reference Types:

It’s not entirely clear what your intent is, from the code and your description. The DB is supposed to be a set of archetypical types of brand new characters?

This looks like the difference between a shallow copy, and a deep copy (as Kurt says, value vs reference). In the prefab line 25, you’re getting a reference to the actual character in the DB, not an independent copy of the character archetype. So when you update things in _character, it updates the database’s archetypical object.

I would have thought you’d want the assignments to be the other way around.

Such as line 28 through 33, I expected to be just _alignment = _character.CombatantType; (copying the data from the archetypical character into this instance).

1 Like

Yep, that’s exactly it. Lines 28 - 33 are there to enable me to change some values around in the editor while I do game design.

Okay cool, I think I get it. That’s what I thought I was doing on line 53 of CharacterDB, but I guess there’s still much more thinking to do here. Thanks all!

We don’t know how your Character class handles the passed arguments in its constructor, however when you pass in an array or list of items and you just store that list in your new Character instance, then all those characters would share the same item list. So changes to one list would be reflected by all other characters as well. Though you were talking about changing the “Name”? of the character which would be quite odd.

While strings are reference types, they actually act like value types. So it’s pretty much impossible to change an existing string that is used in multiple places and have that change reflected everywhere. It’s more likely that at a higher level you have some shared object (like the item list or attributesheet) and you actually change things in that shared object.

1 Like