Press "Enter" to skip to content

Lesson 20.5 – Moving the game logic functions from the UI project to the Engine project

The main goal of this refactoring was to move these functions from SuperAdventure.cs to Player.cs.

  • MoveTo – the function that moves the player to a new location
  • btnUseWeapon_click – to attack a monster
  • btnUsePotion_Click – to use a potion during battle

However, this change could not be done with a few cut-and-pastes.

Those functions contained code for both the game logic and updating the UI. When the code moved to the Player class, it would not have access to the UI controls.

Completing this refactoring required a lot of changes, in a lot of places. This lesson became very long, and very complicated. It was going to take around 30 steps, or more, to include the details for each change.

So, I decided to summarize the changes I made in this lesson.

I suggest you follow this lesson by opening SuperAdventure.cs and Player.cs in Visual Studio, while viewing the updated classes here (GitHub or DropBox).

At the end, you can update your solution with the source code from GitHub or Dropbox.

 

Moving the code to handle button clicks from the UI to the engine

First, I cut-and-pasted the MoveTo() function from SuperAdventure.cs into Player.cs.

That broke the program in about 60 places.

Now, the UI button click functions need to call the MoveTo method in the Player class. I could have just made Player.MoveTo() public, and still called it from the SuperAdventure Move functions. Instead, I decided to create these new functions in Player.cs, with clearer names.

public void MoveNorth()
{
   if(CurrentLocation.LocationToNorth != null)
   {
       MoveTo(CurrentLocation.LocationToNorth);
   }
}
public void MoveEast()
{
   if(CurrentLocation.LocationToEast != null)
   {
       MoveTo(CurrentLocation.LocationToEast);
   }
}
public void MoveSouth()
{
   if(CurrentLocation.LocationToSouth != null)
   {
       MoveTo(CurrentLocation.LocationToSouth);
   }
}
public void MoveWest()
{
   if(CurrentLocation.LocationToWest != null)
   {
       MoveTo(CurrentLocation.LocationToWest);
   }
}

 

Then, I changed the SuperAdventure functions to call the new functions in the Player class.

private void btnNorth_Click(object sender, EventArgs e)
{
   _player.MoveNorth();
}
private void btnEast_Click(object sender, EventArgs e)
{
   _player.MoveEast();
}
private void btnSouth_Click(object sender, EventArgs e)
{
   _player.MoveSouth();
}
private void btnWest_Click(object sender, EventArgs e)
{
   _player.MoveWest();
}

 

For the UseWeapon and UsePotion button click handlers, I created new functions in the Player class. We still need functions in SuperAdventure.cs, to handle the click events. But now, all they will do is get the current weapon or potion from the dropdown and call the new function in the Player class.

This is what they look like now, in SuperAdventure.cs:

private void btnUseWeapon_Click(object sender, EventArgs e)
{
   // Get the currently selected weapon from the cboWeapons ComboBox
   Weapon currentWeapon = (Weapon)cboWeapons.SelectedItem;
   _player.UseWeapon(currentWeapon);
}
private void btnUsePotion_Click(object sender, EventArgs e)
{
   // Get the currently selected potion from the combobox
   HealingPotion potion = (HealingPotion)cboPotions.SelectedItem;
   _player.UsePotion(potion);
}

 

The new Player functions accept the weapon, or potion, as a parameter, instead of reading it from the combobox – because the Player class cannot read the combobox controls of SuperAdventure.cs.

 

Fixing the class-level variables

SuperAdventure.cs holds the Player object in the class-level variable “_player”. The MoveTo, UseWeapon, and UsePotion functions all used the methods and properties of the _player object. Now that the code is inside the Player class, we don’t need to do that anymore.

For example, in the old MoveTo function, we had lines like this:

if(!_player.HasRequiredItemToEnterThisLocation(newLocation))

That line is saying, “for the Player object that we are storing in the _player variable, call the HasRequiredItemToEnterThisLocation function”.

After moving the MoveTo function inside the Player class, we change it to this:

if(!HasRequiredItemToEnterThisLocation(newLocation))

The function knows to look for HasRequiredItemToEnterThisLocation on itself, the Player object it is “inside”.

So, we need to remove all the references to “_player”, from the code we copy-pasted into the Player class. When I did that, it eliminated many errors.

 

We also need to handle the _currentMonster variable that was a class-level variable in SuperAdventure.cs.

We stored the current monster, from the current location, in that variable so we could use it during combat (in the UseWeapon function). But now that the UseWeapon function is in the Player class, we need to make the _currentMonster variable a class-level variable in the Player class.

So, I deleted the _currentMonster declaration that was in SuperAdventure.cs, and added it to Player.cs. That eliminated a few more errors.

 

Notifying the UI of changes

Just like with the previous refactoring lessons, we’re using events to communicate between the Engine classes and the UI.

We now raise a PropertyChanged event when the player’s CurrentLocation changes, just like we did with CurrentHitPoints.

I changed the CurrentLocation auto-property to use a backing variable, and raise an event when it is changed.

private Location _currentLocation;
public Location CurrentLocation
{
   get { return _currentLocation; }
   set
   {
       _currentLocation = value;
       OnPropertyChanged("CurrentLocation");
   }
}

 

The UI sees the event and updates the text showing the current location’s name and description and makes the weapon and potion drop-downs and buttons visible (if there is a monster at the location, and the player has weapons or potions). This is done by adding this code to the PlayerOnPropertyChanged function in SuperAdventure.cs.

if(propertyChangedEventArgs.PropertyName == "CurrentLocation")
{
   // Show/hide available movement buttons
   btnNorth.Visible = (_player.CurrentLocation.LocationToNorth != null);
   btnEast.Visible = (_player.CurrentLocation.LocationToEast != null);
   btnSouth.Visible = (_player.CurrentLocation.LocationToSouth != null);
   btnWest.Visible = (_player.CurrentLocation.LocationToWest != null);
   // Display current location name and description
   rtbLocation.Text = _player.CurrentLocation.Name + Environment.NewLine;
   rtbLocation.Text += _player.CurrentLocation.Description + Environment.NewLine;
   if(_player.CurrentLocation.MonsterLivingHere == null)
   {
       cboWeapons.Visible = false;
       cboPotions.Visible = false;
       btnUseWeapon.Visible = false;
       btnUsePotion.Visible = false;
   }
   else
   {
       cboWeapons.Visible = _player.Weapons.Any();
       cboPotions.Visible = _player.Potions.Any();
       btnUseWeapon.Visible = _player.Weapons.Any();
       btnUsePotion.Visible = _player.Potions.Any();
   }
}

 

That lets us delete any lines in the Player functions that tried to write to rtbLocation, or change the visibility of the buttons. More errors eliminated.

 

Creating a custom event argument

We also need to use a new notification technique. This one will handle everything we display in rtbMessages.

With the previous notifications, the Player class raised an event, and the UI read the new values from the _player object’s properties. We can’t do that with the messages – there is no “message” property in the Player class, with a value for the UI to read.

For the message events, we want to send the text of the message along with the event notification. To do this, we will use a custom event argument class. This is like saying, “This event happened, and here’s all the information you need to know about it.”

Here’s the new class we need to add to the Engine project, MessageEventArgs.cs:

using System;
namespace Engine
{
   public class MessageEventArgs : EventArgs
   {
       public string Message { get; private set; }
       public bool AddExtraNewLine { get; private set; }
       public MessageEventArgs(string message, bool addExtraNewLine)
       {
           Message = message;
           AddExtraNewLine = addExtraNewLine;
       }
   }
}

 

The “: EventArgs” is for this class to inherit from the base EventArgs class, a built-in class for event notifications. All custom event argument classes need to inherit from EventArgs.

There are two auto-properties in the class. They hold the Message, and a Boolean for if we want to add a blank line after the message, for spacing.

In the Player class, our eventhandler code will be a little different from what we used before. It looks like this:

public event EventHandler<MessageEventArgs> OnMessage;

The “EventHandler<MessageEventArgs>” signifies that the Player class will send an event notification with a MessageEventArgs object – the object with the message text we want to display.

Then, we add this function in the Player class, to raise the event:

private void RaiseMessage(string message, bool addExtraNewLine = false)
{
   if(OnMessage != null)
   {
       OnMessage(this, new MessageEventArgs(message, addExtraNewLine));
   }
}

 

When this function raises the event, it passes a MessageEventArgs, with the values the UI code needs.

Inside SuperAdventure.cs, we need to handle these events.

This line is added to SuperAdventure.cs’s constructor, to watch for these events:

_player.OnMessage += DisplayMessage;

And we add this DisplayMessage function to run when the OnMessage event is raised, and we want to add the new message to the UI:

private void DisplayMessage(object sender, MessageEventArgs messageEventArgs)
{
   rtbMessages.Text += messageEventArgs.Message + Environment.NewLine;
   if(messageEventArgs.AddExtraNewLine)
   {
       rtbMessages.Text += Environment.NewLine;
   }
   rtbMessages.SelectionStart = rtbMessages.Text.Length;
   rtbMessages.ScrollToCaret();
}

 

Notice that I also moved the old ScrollToBottom() code into this function. When the UI receives a message event, it will display the text and automatically scroll to the bottom of the rich text box.

Finally, I changed the code in the MoveTo, UseWeapon, and UsePotion functions to raise this event, instead of trying to write directly to rtbMessage.

 

Making the changes in your solution

At this point, everything was working again. I ran the program, and ensured it still worked the same – and it did. The refactoring was done, and it was time to check these changes into source control.

In your version of SuperAdventure, add the new MessageEventArgs class to the Engine project, and copy-paste the refactored code into SuperAdventure.cs and Player.cs. Then run your game, to ensure it works.

If you have any problems, please leave a comment and I’ll try to help you.

 

Summary

Sometimes, when you start refactoring, you need to make a lot of changes to get the program running again. But, if you’re doing refactoring correctly, you end up with much better code.

For example, after these changes, we could probably write a WPF or ASP.Net (web-based) version of the game very quickly. You would only need to create a new UI project (a WPF app, or an ASP.Net web app), add a reference to Engine, and create a simple UI page that does the same type of binding that SuperAdventure.cs does.

Also, by moving all the logic into a class library, we could easily create unit tests to ensure that our classes work correctly.

 

Source code for this lesson

Source code on GitHub

Source code on Dropbox

 

Next lesson: Lesson 21.0 – Plans for adding a vendor to locations

Previous lesson: Lesson 20.4 – Binding child list properties to a combobox

All lessons: Learn C# by Building a Simple RPG Index

19 Comments

  1. Zac
    Zac March 30, 2016

    I believe some editing may be needed for the following:

    “So, I moved deleted the _currentMonster declaration in SuperAdventure.cs, and added it to Player.cs. That eliminated a few more errors.”

  2. J
    J May 27, 2016

    Hi Scott,

    This lesson broke alot in my code, due to some changes that I wrote myself (for example I don’t auto load and save the game, I’m using buttons with event handlers for loading, saving and new games). I’m in the process of fixing it now, but here are some thoughts that occurred to me while following this lesson:

    You write that instead of making moveto public, you wrote the movenorth, south, etc. methods to call the MoveTo method. This got me struggling in the forms code where _player.MoveTo is called directly, this had to be replaced by another public method that would access the private MoveTo method. So I cheated a little and read that part in your code to see what you did, only to find out you made MoveTo public after all. 🙂

    And:

    The Gold and ExperiencePoints property setters both call the OnPropertyChanged() method to fire the PropertyChanged event in LivingCreature, yet the labels for those properties seem to be bound directly to the properties using the DataBindings.Add method of those labels (this.lblHitPoints.DataBindings.Add(“Text”, _player, “CurrentHitPoints”);) rather than being subscribed to the PropertyChanged event?

    Might be missing something in my knowledge here. I’m not that skilled in events yet. Could you help me out a bit here?

    Thanks!

    • Scott Lilly
      Scott Lilly May 29, 2016

      When I said I could have made the Player MoveTo function public, I meant “I could have done that, and not done anything to the SuperAdventure MoveNorth/East/South/West functions. However, I’d like to make the MoveNorth/East/South/West functions a little better, and less-likely to allow bad input (for example, calling MoveNorth, when there is no location to the North).”

      Doing a DataBindings.Add is a different way of subscribing to an event. Instead of using that, we could have subscribed to the PropertyChanged event. It’s just two different ways to subscribe to an event.

  3. J
    J May 30, 2016

    Hi Scott,

    I hope you would help me out a bit… I worked around all the errors, mostly using your code, while understanding what is happening. In some places I have my own version of the code to support the stuff I changed. One thing I noticed is that the selected weapon in the combobox changes back to rusty sword sometimes. It seems to happen when I loot another weapon (I went to kill snakes – they dropped a Club, then i proceed to kill more snakes using the club, but when another club drops quantity of club will be 2 and I have to manually select the club again or I’ll be using the rusty sword).

    I can’t seem to figure out why this is happening just yet. Maybe you would take a look at it?

    LINK REMOVED FOR PRIVACY

    Thanks alot.

    J

  4. J
    J May 30, 2016

    I found out using the debugger that it happens after the form calls cboWeapons.DataSource = _player.Weapons; in the PlayerOnPropertyChanged method. But it should invoke the selectedindexchanged event handler and use the players currentweapon property which should be set to Club still?! I feel kinda stupid here.. -.-

    • Scott Lilly
      Scott Lilly May 30, 2016

      Try adding this code after resetting the cboWeapons.DataSource to the updated _player.Weapons (in PlayerOnPropertyChanged – line 252):

      if (_player.Weapons.Any() && _player.CurrentWeapon != null)
      {
      this.cboWeapons.SelectedItem = _player.CurrentWeapon;
      }

      Because the cboWeapons DataSource is being re-populated, I believe it might be clearing/resetting the SelectedItem for the combobox. Please let me know if that works (I can’t work on my local version of the program right now).

  5. J
    J May 31, 2016

    No this didn’t seem to fix it. To be honest I think I had tried the same statement already yesterday, but without the if check to see if there is any weapon in the weapons list and the null check…

    I’ll try and figure it out later, right now I’m on the part where you create a vendor and having alot of fun in the process 🙂

    Thanks Scott, if it wasn’t for your tut. I’d still have a hard time putting all my theoretical knowledge (from C# books) into practice. This helps me out alot.

    BTW any tips on what I could do after your tutorial? I was thinking of creating my own local IMDB application with WPF to give personal ratings to my movie collection on my harddisk and be able to delete everything that I rated below a certain number with a click (incase my harddrive gets full).

    • Scott Lilly
      Scott Lilly May 31, 2016

      I’ll take a look at it later this week. If I find the problem, I’ll let you know (and update the lesson’s code, if needed).

      For the next step, I’d suggest an app on a subject you like – if you like movies, an IMDB type of app is a good choice. Whenever I learn a new language, I write a program to find prime numbers. That’s something I’m interested in, and I understand the logic needed to solve the problem. So, I only need to concentrate on learning the new syntax/language. WPF is definitely a good thing to learn next. Windows Forms is simpler to start with, but WPF is more common in current programs.

  6. J
    J May 31, 2016

    Obviously while building that I will deliberately try and incorporate as much stuff from Microsoft Visual C# 2013 step by step book that I learnt.

  7. Trevor
    Trevor October 4, 2016

    I did all the same code as you (plus on small tweak to add a map button [does not impact refactoring or any other part of code]) and when I ran the project the SuperAdventure.Designer.cs had an error in messages and the solutions Visual Studio suggested made it build okay but running the game threw an exception.

     

    The code in the designer area was in the rtbmessages section:

    this.rtbMessages.TextChanged += new System.EventHandler(this.rtbMessages_TextChanged);

     

    I wanted me to add this to the end of the program:

    private System.EventHandler rtbMessages_TextChanged;

    What I dont get is that this was not mentioned in the lesson as things we needed to fix so I am not sure what to do.

    • Scott Lilly
      Scott Lilly October 6, 2016

      It sounds like you might have accidentally double-clicked on the Messages RichTextBox, while on the UI Designer screen for SuperAdventure. That would have created a TextChanged eventhandler, which doesn’t have the associated function in the code you would have pasted in.

      You can edit the SuperAdventure.Designer.cs file, and either comment out the line (put double-slashes “//” at the front of it), or delete that line. There is some more information on the Designer file, and eventhandlers, in Lesson 21.3.

      Please tell me if that does not fix the error.

    • Scott Lilly
      Scott Lilly December 10, 2017

      Can you upload your complete solution (including the directories under it, and all the files in those directories)? I need to build the complete solution, to find the source of the error.

  8. NOTaROBOT
    NOTaROBOT February 9, 2018

    Hi Scott!
    I want to pass this lesson and i’ve got some disturbing questions :
    1) How is it important to know in general? I don’t have any experience as a programmer and i don’t know will the clients request this kind of thing and how the programmers will look at my code if don’t do that.
    2) As you may guess from my stupid questions, i’m a begginer and i how is this necessary to know for me right now? Maybe in the future when i’ll become an advanced programmer i will understand that more fully.
    Thank you for your lessons!

    • Scott Lilly
      Scott Lilly February 9, 2018

      You’re welcome!

      1) If you write a program for a business-person, they will probably not know if you used good programming habits or bad ones. They will only know if the program does what they want. However, if other programmers will see the code, they will know the program is not high-quality (unless they are bad programmers). Plus, you will have fewer bugs with well-written code, and it will be easier to maintain in the future. So, it is a good thing to learn and do in your programs.
      2) You do not need to be an expert at the beginning. But, you should do your best, and always learn how to be better. I have seen some programs that cost millions of dollars more than they should have cost – because the code is so bad, it is almost impossible to work on without breaking something else. Nobody was happy working on those projects.

      After you finish these lessons, you might want to look at the WPF RPG lessons. It is basically the same game, but with a WPF user interface and a much better “architecture”. You will probably see the code is much cleaner and easier to work with.

  9. Daniel Hall
    Daniel Hall June 6, 2018

    Ok so I just wanted to make a comment since there were some issues that I was having but figured them out. So first off I want to say I am brand new at this. Secondly this is my first program that I have written with your help Scott, so for that Thank you for these lessons. Thirdly I was having an issue with the MaximumHitPoints not staying where I initially set them. Well this was because in this line of code:

    public void AddExperiencePoints(int experiencePointsToAdd)
    {
    ExperiencePoints += experiencePointsToAdd;
    MaximumHitPoints = (Level * 50);
    }

    my value was set lower then what you see here. So I changed it to what you see now, my MaximumHitPoints value now stays at the initial value since 50 was the initial value for the player hit points. I didn’t realize where the problem was until i went through it line by line searching for an error that wasn’t an error.

    • Scott Lilly
      Scott Lilly June 7, 2018

      It’s great that you were able to track down the source of the problem! Debugging usually isn’t fun, but it’s a vital skill to learn and practice.

  10. Kay
    Kay December 18, 2019

    Dear Scott Lilly:

    I am getting two very similar error messages, both in the Super Adventure class. One of them says that there is no definition for “rtbMessages_TextChanged”, and another that says the same for “SuperAdventure_Load”. Here is latest source code for your convenience:

    https://github.com/dolly-kay/Game-Codes/tree/master/SuperAdventure

Leave a Reply

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