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.

 

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

 

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:

 

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.

 

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.

 

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:

 

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:

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:

 

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:

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:

 

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

Get it from GitHub: https://gist.github.com/ScottLilly/83d3fc7d0b0381dfd33e

Or DropBox: https://www.dropbox.com/sh/fynhmq31lfjyu4r/AABfd5Eib4krUwluv1WmNe6La?dl=0

 

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

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

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

14 thoughts on “Lesson 20.5 – Moving the game logic functions from the UI project to the Engine project

  1. 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. 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!

    1. 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. 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. 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.. -.-

    1. 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. 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).

    1. 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. 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. 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.

    1. 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.

    1. 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.

Leave a Reply

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