Press "Enter" to skip to content

Lesson 12.2: Creating the AttackWithWeapon command

In this lesson, we’ll build the first class that uses the Command Design Pattern – a class to handle the player attacking the current monster.

Step 1: Create Engine\Actions\AttackWithWeapon.cs

In the Engine project, create a new Actions folder and create a new class in it named AttackWithWeapon.cs. This will be a “command” class, from the Command design pattern.

Add the public event “OnActionPerformed” (line 12). We’ll use this event to notify the UI of any messages that result from executing this command object. The “ReportResult” function (lines 51-54) is the function that raises the event notification – if anything subscribed to the OnActionPerformed event.

In the constructor, we check that the parameters are valid and then save them to private variables.

The “Execute” function (lines 36-49) is the code to run when we want to execute this command. It accepts two parameters – the “actor” (who is performing the action) and the “target” (who is having the action done to them).

The code inside Execute is basically the combat code we had in the GameSession class when the player attacked the CurrentMonster.

We have some unused parameters in this class: “weapon” in the constructor and “actor” in the Execute function. We’ll use those in the future, as we make the combat logic more complex.

NOTE: On line 46, we raise the event that the UI will eventually display – before applying the damage to the monster. This is another one of those situations to watch for when usng events. The TakeDamage function could result in killing the monster, which raises an OnKilled event.

We want to be sure our “You hit the monster for X damage” message gets to the UI before the OnKilled event does. That’s why we do the action’s ReportResult notification before applying the damage to the monster.

AttackWithWeapon.cs
using System;
using Engine.Models;
namespace Engine.Actions
{
    public class AttackWithWeapon
    {
        private readonly GameItem _weapon;
        private readonly int _maximumDamage;
        private readonly int _minimumDamage;
        public event EventHandler<string> OnActionPerformed;
        public AttackWithWeapon(GameItem weapon, int minimumDamage, int maximumDamage)
        {
            if(weapon.Category != GameItem.ItemCategory.Weapon)
            {
                throw new ArgumentException($"{weapon.Name} is not a weapon");
            }
            if(_minimumDamage < 0)
            {
                throw new ArgumentException("minimumDamage must be 0 or larger");
            }
            if(_maximumDamage < _minimumDamage)
            {
                throw new ArgumentException("maximumDamage must be >= minimumDamage");
            }
            _weapon = weapon;
            _minimumDamage = minimumDamage;
            _maximumDamage = maximumDamage;
        }
        public void Execute(LivingEntity actor, LivingEntity target)
        {
            int damage = RandomNumberGenerator.NumberBetween(_minimumDamage, _maximumDamage);
            if(damage == 0)
            {
                ReportResult($"You missed the {target.Name.ToLower()}.");
            }
            else
            {
                ReportResult($"You hit the {target.Name.ToLower()} for {damage} points.");
                target.TakeDamage(damage);
            }
        }
        private void ReportResult(string result)
        {
            OnActionPerformed?.Invoke(this, result);
        }
    }
}

Step 2: Modify Engine\Models\GameItem.cs

Remove the MinimumDamage and MaximumDamage properties from the class – including from the constructor and Clone function.

Add a new “Action” property whose datatype is “AttackWithWeapon”. Add this new property to the constructor and Clone function.

Add the new PerformAction function (lines 31-34). When the player uses their CurrentWeapon, this is the function we will call.

GameItem.cs
using Engine.Actions;
namespace Engine.Models
{
    public class GameItem
    {
        public enum ItemCategory
        {
            Miscellaneous,
            Weapon
        }
        public ItemCategory Category { get; }
        public int ItemTypeID { get; }
        public string Name { get; }
        public int Price { get; }
        public bool IsUnique { get; }
        public AttackWithWeapon Action { get; set; }
        public GameItem(ItemCategory category, int itemTypeID, string name, int price,
                        bool isUnique = false, AttackWithWeapon action = null)
        {
            Category = category;
            ItemTypeID = itemTypeID;
            Name = name;
            Price = price;
            IsUnique = isUnique;
            Action = action;
        }
        public void PerformAction(LivingEntity actor, LivingEntity target)
        {
            Action?.Execute(actor, target);
        }
        public GameItem Clone()
        {
            return new GameItem(Category, ItemTypeID, Name, Price, IsUnique, Action);
        }
    }
}

Step 3: Modify Engine\Factories\ItemFactory.cs

Since we changed the constructor for the GameItem class, we need to change the ItemFactory.

First, on line 38, we’ll remove the minimumDamage and maximumDamage parameters from the constructor call and store the GameItem in a variable.

On line 40, we’ll create an AttackWithWeapon object and set it to the weapon’s Action property. We need to have these on two separate lines because the AttackWithWeapon constructor needs the weapon passed in as a parameter.

NOTE: To use the AttackWithWeapon class, you need to add the “using Engine.Actions” statement on line 3.

ItemFactory.cs
using System.Collections.Generic;
using System.Linq;
using Engine.Actions;
using Engine.Models;
namespace Engine.Factories
{
    public static class ItemFactory
    {
        private static readonly List<GameItem> _standardGameItems = new List<GameItem>();
        static ItemFactory()
        {
            BuildWeapon(1001, "Pointy Stick", 1, 1, 2);
            BuildWeapon(1002, "Rusty Sword", 5, 1, 3);
            BuildMiscellaneousItem(9001, "Snake fang", 1);
            BuildMiscellaneousItem(9002, "Snakeskin", 2);
            BuildMiscellaneousItem(9003, "Rat tail", 1);
            BuildMiscellaneousItem(9004, "Rat fur", 2);
            BuildMiscellaneousItem(9005, "Spider fang", 1);
            BuildMiscellaneousItem(9006, "Spider silk", 2);
        }
        public static GameItem CreateGameItem(int itemTypeID)
        {
            return _standardGameItems.FirstOrDefault(item => item.ItemTypeID == itemTypeID)?.Clone();
        }
        private static void BuildMiscellaneousItem(int id, string name, int price)
        {
            _standardGameItems.Add(new GameItem(GameItem.ItemCategory.Miscellaneous, id, name, price));
        }
        private static void BuildWeapon(int id, string name, int price, 
                                        int minimumDamage, int maximumDamage)
        {
            GameItem weapon = new GameItem(GameItem.ItemCategory.Weapon, id, name, price, true);
            weapon.Action = new AttackWithWeapon(weapon, minimumDamage, maximumDamage);
            _standardGameItems.Add(weapon);
        }
    }
}

Step 4: Modify Engine\Models\LivingEntity.cs

First, we’ll add an “OnActionPerformed” event on line 101. The UI will watch this event for any messages that are raised when the LivingEntity performs an action. The LivingEntity watches the AttackWithWeapon class for any messages. If it sees one, LivingEntity will raise an event that passes the AttackWithWeapon message to anything subscribed to the LivingEntity’s OnActionPerformed event.

You can think of this as an employee notifying their manager of something, and the manager notifying their boss.

Next, because we are changing the program to use The AttackWIthWeapon “Action” property on the GameItem class, we need to let the monsters have a weapon. This means we need to move the CurrentWeapon from the GameSession class (where it only applies to the CurrentPlayer) into the LivingEntity class. This way, Monsters and Traders will also have use the a CurrentWeapon.

Add the “_currentWeapon” backing variable on line 17. Then, add the CurrentWeapon property setter and getter on lines 69-88. Inside the setter, subscribe to (and unsubscribe from) the current weapon’ action message event. This is where the player (or monster/trader) watches for events raised by their weapon’s action.

Create the “RaiseActionPerformedEvent” function on lines 214-218, to pass the weapon’s message up to the UI – which is only watching for events on the LivingEntity, not the LivingEntity’s CurrentWeapon.

Finally, create the “UseCurrentWeaponOn” function on lines 117-120. This is just a simple wrapper function that the ViewModel will use to initiate an attack. We could eliminate this function, and have the ViewModel directly call CurrentPlayer.CurrentWeapon.PerformAction(CurrentPlayer, CurrentMonster). But, I think this makes the code a little cleaner.

LivingEntity.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace Engine.Models
{
    public abstract class LivingEntity : BaseNotificationClass
    {
        #region Properties
        private string _name;
        private int _currentHitPoints;
        private int _maximumHitPoints;
        private int _gold;
        private int _level;
        private GameItem _currentWeapon;
        public string Name
        {
            get { return _name; }
            private set
            {
                _name = value;
                OnPropertyChanged();
            }
        }
        public int CurrentHitPoints
        {
            get { return _currentHitPoints; }
            private set
            {
                _currentHitPoints = value;
                OnPropertyChanged();
            }
        }
        public int MaximumHitPoints
        {
            get { return _maximumHitPoints; }
            protected set
            {
                _maximumHitPoints = value;
                OnPropertyChanged();
            }
        }
        public int Gold
        {
            get { return _gold; }
            private set
            {
                _gold = value;
                OnPropertyChanged();
            }
        }
        public int Level
        {
            get { return _level; }
            protected set
            {
                _level = value;
                OnPropertyChanged();
            }
        }
        public GameItem CurrentWeapon
        {
            get { return _currentWeapon; }
            set
            {
                if(_currentWeapon != null)
                {
                    _currentWeapon.Action.OnActionPerformed -= RaiseActionPerformedEvent;
                }
                _currentWeapon = value;
                if (_currentWeapon != null)
                {
                    _currentWeapon.Action.OnActionPerformed += RaiseActionPerformedEvent;
                }
                OnPropertyChanged();
            }
        }
        public ObservableCollection<GameItem> Inventory { get; }
        public ObservableCollection<GroupedInventoryItem> GroupedInventory { get; }
        public List<GameItem> Weapons =>
            Inventory.Where(i => i.Category == GameItem.ItemCategory.Weapon).ToList();
        public bool IsDead => CurrentHitPoints <= 0;
        #endregion
        public event EventHandler<string> OnActionPerformed;
        public event EventHandler OnKilled;
        protected LivingEntity(string name, int maximumHitPoints, int currentHitPoints, 
                               int gold, int level = 1)
        {
            Name = name;
            MaximumHitPoints = maximumHitPoints;
            CurrentHitPoints = currentHitPoints;
            Gold = gold;
            Level = level;
            Inventory = new ObservableCollection<GameItem>();
            GroupedInventory = new ObservableCollection<GroupedInventoryItem>();
        }
        public void UseCurrentWeaponOn(LivingEntity target)
        {
            CurrentWeapon.PerformAction(this, target);
        }
        public void TakeDamage(int hitPointsOfDamage)
        {
            CurrentHitPoints -= hitPointsOfDamage;
            if(IsDead)
            {
                CurrentHitPoints = 0;
                RaiseOnKilledEvent();
            }
        }
        public void Heal(int hitPointsToHeal)
        {
            CurrentHitPoints += hitPointsToHeal;
            if(CurrentHitPoints > MaximumHitPoints)
            {
                CurrentHitPoints = MaximumHitPoints;
            }
        }
        public void CompletelyHeal()
        {
            CurrentHitPoints = MaximumHitPoints;
        }
        public void ReceiveGold(int amountOfGold)
        {
            Gold += amountOfGold;
        }
        public void SpendGold(int amountOfGold)
        {
            if(amountOfGold > Gold)
            {
                throw new ArgumentOutOfRangeException($"{Name} only has {Gold} gold, and cannot spend {amountOfGold} gold");
            }
            Gold -= amountOfGold;
        }
        public void AddItemToInventory(GameItem item)
        {
            Inventory.Add(item);
            if(item.IsUnique)
            {
                GroupedInventory.Add(new GroupedInventoryItem(item, 1));
            }
            else
            {
                if(!GroupedInventory.Any(gi => gi.Item.ItemTypeID == item.ItemTypeID))
                {
                    GroupedInventory.Add(new GroupedInventoryItem(item, 0));
                }
                GroupedInventory.First(gi => gi.Item.ItemTypeID == item.ItemTypeID).Quantity++;
            }
            OnPropertyChanged(nameof(Weapons));
        }
        public void RemoveItemFromInventory(GameItem item)
        {
            Inventory.Remove(item);
            GroupedInventoryItem groupedInventoryItemToRemove = item.IsUnique ? 
                GroupedInventory.FirstOrDefault(gi => gi.Item == item) : 
                GroupedInventory.FirstOrDefault(gi => gi.Item.ItemTypeID == item.ItemTypeID);
            if(groupedInventoryItemToRemove != null)
            {
                if(groupedInventoryItemToRemove.Quantity == 1)
                {
                    GroupedInventory.Remove(groupedInventoryItemToRemove);
                }
                else
                {
                    groupedInventoryItemToRemove.Quantity--;
                }
            }
            OnPropertyChanged(nameof(Weapons));
        }
        #region Private functions
        private void RaiseOnKilledEvent()
        {
            OnKilled?.Invoke(this, new System.EventArgs());
        }
        private void RaiseActionPerformedEvent(object sender, string result)
        {
            OnActionPerformed?.Invoke(this, result);
        }
        #endregion
    }
}

Step 5: Modify Engine\ViewModels\GameSession.cs

Remove the CurrentWeapon property from line 101 (in the original code).

Because we now watch for action results as LivingEntity OnActionPerformed events, we need to subscribe to the event and create a handler in the ViewModel.

Create OnCurrentPlayerPerformedAction (lines 279-282). Subscribe to it on line 38 and unsubscribe from it on line 29.

Change the AttackCurrentMoster function to use the new action.

Change line 249 to check CurrentPlayer.CurrentWeapon – since it is not a property in GameSession.

Change the existing code that handles the player attacking the monster (lines 255-266, in the original code) to the new line 255.

GameSession.cs
using System;
using System.Linq;
using Engine.EventArgs;
using Engine.Factories;
using Engine.Models;
namespace Engine.ViewModels
{
    public class GameSession : BaseNotificationClass
    {
        public event EventHandler<GameMessageEventArgs> OnMessageRaised;
        #region Properties
        private Player _currentPlayer;
        private Location _currentLocation;
        private Monster _currentMonster;
        private Trader _currentTrader;
        public World CurrentWorld { get; }
        public Player CurrentPlayer
        {
            get { return _currentPlayer; }
            set
            {
                if(_currentPlayer != null)
                {
                    _currentPlayer.OnActionPerformed -= OnCurrentPlayerPerformedAction;
                    _currentPlayer.OnLeveledUp -= OnCurrentPlayerLeveledUp;
                    _currentPlayer.OnKilled -= OnCurrentPlayerKilled;
                }
                _currentPlayer = value;
                if (_currentPlayer != null)
                {
                    _currentPlayer.OnActionPerformed += OnCurrentPlayerPerformedAction;
                    _currentPlayer.OnLeveledUp += OnCurrentPlayerLeveledUp;
                    _currentPlayer.OnKilled += OnCurrentPlayerKilled;
                }
            }
        }
        public Location CurrentLocation
        {
            get { return _currentLocation; }
            set
            {
                _currentLocation = value;
                OnPropertyChanged();
                OnPropertyChanged(nameof(HasLocationToNorth));
                OnPropertyChanged(nameof(HasLocationToEast));
                OnPropertyChanged(nameof(HasLocationToWest));
                OnPropertyChanged(nameof(HasLocationToSouth));
                CompleteQuestsAtLocation();
                GivePlayerQuestsAtLocation();
                GetMonsterAtLocation();
                CurrentTrader = CurrentLocation.TraderHere;
            }
        }
        public Monster CurrentMonster
        {
            get { return _currentMonster; }
            set
            {
                if(_currentMonster != null)
                {
                    _currentMonster.OnKilled -= OnCurrentMonsterKilled;
                }
                
                _currentMonster = value;
                if(_currentMonster != null)
                {
                    _currentMonster.OnKilled += OnCurrentMonsterKilled;
                    RaiseMessage("");
                    RaiseMessage($"You see a {CurrentMonster.Name} here!");
                }
                OnPropertyChanged();
                OnPropertyChanged(nameof(HasMonster));
            }
        }
        public Trader CurrentTrader
        {
            get { return _currentTrader; }
            set
            {
                _currentTrader = value; 
                
                OnPropertyChanged();
                OnPropertyChanged(nameof(HasTrader));
            }
        }
        public bool HasLocationToNorth => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1) != null;
        public bool HasLocationToEast => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate) != null;
        public bool HasLocationToSouth => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1) != null;
        public bool HasLocationToWest => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate) != null;
        public bool HasMonster => CurrentMonster != null;
        public bool HasTrader => CurrentTrader != null;
        #endregion
        public GameSession()
        {
            CurrentPlayer = new Player("Scott", "Fighter", 0, 10, 10, 1000000);
            if (!CurrentPlayer.Weapons.Any())
            {
                CurrentPlayer.AddItemToInventory(ItemFactory.CreateGameItem(1001));
            }
            CurrentWorld = WorldFactory.CreateWorld();
            CurrentLocation = CurrentWorld.LocationAt(0, 0);
        }
        public void MoveNorth()
        {
            if(HasLocationToNorth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1);
            }
        }
        public void MoveEast()
        {
            if(HasLocationToEast)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate);
            }
        }
        public void MoveSouth()
        {
            if(HasLocationToSouth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1);
            }
        }
        public void MoveWest()
        {
            if(HasLocationToWest)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate);
            }
        }
        private void CompleteQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                QuestStatus questToComplete =
                    CurrentPlayer.Quests.FirstOrDefault(q => q.PlayerQuest.ID == quest.ID &&
                                                             !q.IsCompleted);
                if(questToComplete != null)
                {
                    if(CurrentPlayer.HasAllTheseItems(quest.ItemsToComplete))
                    {
                        // Remove the quest completion items from the player's inventory
                        foreach (ItemQuantity itemQuantity in quest.ItemsToComplete)
                        {
                            for(int i = 0; i < itemQuantity.Quantity; i++)
                            {
                                CurrentPlayer.RemoveItemFromInventory(CurrentPlayer.Inventory.First(item => item.ItemTypeID == itemQuantity.ItemID));
                            }
                        }
                        RaiseMessage("");
                        RaiseMessage($"You completed the '{quest.Name}' quest");
                        // Give the player the quest rewards
                        RaiseMessage($"You receive {quest.RewardExperiencePoints} experience points");
                        CurrentPlayer.AddExperience(quest.RewardExperiencePoints);
                        RaiseMessage($"You receive {quest.RewardGold} gold");
                        CurrentPlayer.ReceiveGold(quest.RewardGold);
                        foreach(ItemQuantity itemQuantity in quest.RewardItems)
                        {
                            GameItem rewardItem = ItemFactory.CreateGameItem(itemQuantity.ItemID);
                            RaiseMessage($"You receive a {rewardItem.Name}");
                            CurrentPlayer.AddItemToInventory(rewardItem);
                        }
                        // Mark the Quest as completed
                        questToComplete.IsCompleted = true;
                    }
                }
            }
        }
        private void GivePlayerQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID))
                {
                    CurrentPlayer.Quests.Add(new QuestStatus(quest));
                    RaiseMessage("");
                    RaiseMessage($"You receive the '{quest.Name}' quest");
                    RaiseMessage(quest.Description);
                    RaiseMessage("Return with:");
                    foreach(ItemQuantity itemQuantity in quest.ItemsToComplete)
                    {
                        RaiseMessage($"   {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
                    }
                    RaiseMessage("And you will receive:");
                    RaiseMessage($"   {quest.RewardExperiencePoints} experience points");
                    RaiseMessage($"   {quest.RewardGold} gold");
                    foreach(ItemQuantity itemQuantity in quest.RewardItems)
                    {
                        RaiseMessage($"   {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
                    }
                }
            }
        }
        private void GetMonsterAtLocation()
        {
            CurrentMonster = CurrentLocation.GetMonster();
        }
        public void AttackCurrentMonster()
        {
            if(CurrentPlayer.CurrentWeapon == null)
            {
                RaiseMessage("You must select a weapon, to attack.");
                return;
            }
            CurrentPlayer.UseCurrentWeaponOn(CurrentMonster);
            if(CurrentMonster.IsDead)
            {
                // Get another monster to fight
                GetMonsterAtLocation();
            }
            else
            {
                // Let the monster attack
                int damageToPlayer = RandomNumberGenerator.NumberBetween(CurrentMonster.MinimumDamage, CurrentMonster.MaximumDamage);
                if (damageToPlayer == 0)
                {
                    RaiseMessage($"The {CurrentMonster.Name} attacks, but misses you.");
                }
                else
                {
                    RaiseMessage($"The {CurrentMonster.Name} hit you for {damageToPlayer} points.");
                    CurrentPlayer.TakeDamage(damageToPlayer);
                }
            }
        }
        private void OnCurrentPlayerPerformedAction(object sender, string result)
        {
            RaiseMessage(result);
        }
        private void OnCurrentPlayerKilled(object sender, System.EventArgs eventArgs)
        {
            RaiseMessage("");
            RaiseMessage("You have been killed.");
            CurrentLocation = CurrentWorld.LocationAt(0, -1);
            CurrentPlayer.CompletelyHeal();
        }
        private void OnCurrentMonsterKilled(object sender, System.EventArgs eventArgs)
        {
            RaiseMessage("");
            RaiseMessage($"You defeated the {CurrentMonster.Name}!");
            RaiseMessage($"You receive {CurrentMonster.RewardExperiencePoints} experience points.");
            CurrentPlayer.AddExperience(CurrentMonster.RewardExperiencePoints);
            RaiseMessage($"You receive {CurrentMonster.Gold} gold.");
            CurrentPlayer.ReceiveGold(CurrentMonster.Gold);
            foreach(GameItem gameItem in CurrentMonster.Inventory)
            {
                RaiseMessage($"You receive one {gameItem.Name}.");
                CurrentPlayer.AddItemToInventory(gameItem);
            }
        }
        private void OnCurrentPlayerLeveledUp(object sender, System.EventArgs eventArgs)
        {
            RaiseMessage($"You are now level {CurrentPlayer.Level}!");
        }
        private void RaiseMessage(string message)
        {
            OnMessageRaised?.Invoke(this, new GameMessageEventArgs(message));
        }
    }
}

Step 6: Modify WPFUI\MainWindow.xaml

Update the player’s weapon combobox to bind its SelectedItem to “CurrentPlayer.CurrentWeapon” (line 234), instead of the “CurrentWeapon” that used to exist in the GameSession class.

MainWindow.xaml (lines 232-236)
                <ComboBox Grid.Row="0" Grid.Column="0"
                          ItemsSource="{Binding CurrentPlayer.Weapons}"
                          SelectedItem="{Binding CurrentPlayer.CurrentWeapon}"
                          DisplayMemberPath="Name"
                          SelectedValuePath="ID"/>

Step 7: Run the test and the game, to ensure it still works.

In the next lesson, we’ll make changes to allow us to handle more actions and make the AttackWithWeapon action more flexible – so the monsters can use it too.

NEXT LESSON: Lesson 12.3: Making the Action class more flexible with an interface

PREVIOUS LESSON: Lesson 12.1: Making the GameItem class more flexible

4 Comments

  1. wowzers
    wowzers 2024-02-18

    Thank you very much Scott for putting so much work into these videos, they are very enlightening. I’ve been following along and typing out most of the changes, sometimes pausing to see if I can figure out the solution before unpausing. The way you’re bubbling up the events from the weapon action is a great design, and I’m very curious to see how the initial architecting process went. If you have any resources that would help develop the skill of architecting such cool systems, I’m all ears. It can’t really just be some flowcharts and UML diagrams can it? Thanks again!

    • SOSCSRPG
      SOSCSRPG 2024-02-19

      You’re welcome! I can’t say there is any specific resources that helped that helped come up with that design. It’s a lot of what I’ve learned over the years.

      You could definitely learn about design patterns. But, once you learn about them, don’t feel like you have to use all of them, all of the time. They just give you a mental model of types of problems you may run across, and possible solutions for those problems. I also like some of the older books by Steve McConnell – they discuss the mindset behind being a professional developer. The strategies and ways of thinking are the important things to learn, not just the tricks and tactics.

      I’ve also liked reading about process improvements, especially in things like manufacturing. I recommend books by Eli Goldratt, Edward Deming, and anything about the Toyota Production System. Much of that is what led to Agile and Scrum for programmers. Try thinking about data moving through a program like a physical product moving through a manufacturing plant. Trying to visualize data like something physical may help you identify inefficiencies in a program. You picture people moving a box of data back-and-forth multiple times and realize it could be done in one trip.

      Another thing that has helped is trying out ideas in small test projects. If you read about a programming technique, or get an idea, write a small little test program to see how it really works – its good points and bad points. And when you get something that works, see if there is a better/simpler way to do it. Nowadays, you can use something like ChatGPT to paste in some code and ask it, “Is there a better way to do this?”

      The important thing is to keep learning things, keep trying things, and apply what works to your programs. Like Bruce Lee’s quote, “Absorb what is useful, discard what is useless and add what is specifically your own”

  2. Tristan
    Tristan 2024-04-03

    After implementing this lesson I started getting binding errors for my image name, but only the image name and only in the xaml file when i run the game

    Severity Count Data Context Binding Path Target Target Type Description File Line Project Detail Description
    Error 1 GameSession CurrentLocation.ImageName Image.Source ImageSource Failed to convert value ‘/Engine;component/Images/Locations//Engine;component/Images/Locations/TownSquare.png’ (type ‘String’) to the target type using converter ‘TargetDefaultValueConverter’. The fallback value will be used if it’s available. IOException:’System.IO.IOException: Cannot locate resource ‘images/locations//engine;component/images/locations/townsquare.png’. IOException:’System.IO.IOException: Cannot locate resource ‘images/locations//engine;component/images/locations/townsquare.png’.

    • SOSCSRPG
      SOSCSRPG 2024-04-04

      Hi Tristan,

      When you run the game do you see the images? Sometimes the XAML editor shows errors that aren’t really errors.

      If you don’t see the images when running the game, check the image files “Properties” in Visual Studio and make sure they are marked as “Copy Always”.

      If that doesn’t solve it, can you upload your solution (including the directories under it, and all the files in those directories) to GitHub, Dropbox, or some other file-sharing location so I can look at it?

      If you haven’t used GitHub before, here is a video on how to upload your solution to GitHub and share it with me. https://youtu.be/0si9ElYQv8I

Leave a Reply

Your email address will not be published. Required fields are marked *