Several people have asked about adding graphics to this game. In this lesson, I’ll show how to create a World Map screen, and display images for the locations.
This is different from the way I display images in a WPF program (like in the “Build a C#/WPF RPG” lessons). WPF is a little easier.
I’m working on a second lesson for these graphics, to show how to only show the images for the locations the player has visited.
Step 1: Add the location image files to the SuperAdventure project.
Download the zip file below, which has image files to use for the locations built in to the default game. After you download the zip file, uncompress it and remember where you store the uncompressed image files.
These are all 125 x 125 pixel PNG images, with transparent backgrounds. In this lesson, we will display them in 75 x 75 pixel squares, and re-size the images to fit.
In the SuperAdventure project, create a new “Images” folder.
Add all the images from the zip file by right-clicking on the Images folder and selecting Add -> Existing Item…
After the images are added to the project, right-click on each of them, and select “Properties”.
Set “Build Action” to “Embedded Resource”. This will include the images as part of the assembly file (program), when Visual Studio builds the solution. Also set “Copy to Output Directory” to “Do not copy”. You don’t need to copy the files, because they will be embedded in the program.
Step 2: Create a new Windows Form in the SuperAdventure project, named WorldMap.
Right now, you don’t need to do anything with it. We will do more with it in Step 4.
Step 3: Modify SuperAdventure.cs, in Design mode.
Add a new button to display the WorldMap window. I created a new button with these properties:
Name: btnMap
Text: Map
Location: 494, 457
Size: 75, 23
This can put this new button in the middle of the four movement buttons. You might need to adjust the locations of the existing movement buttons, to make the buttons look nice.
Double-click on the new button, so Visual Studio will create the eventhandler in SuperAdventure.Designer.cs and the new btnMap_Click function in SuperAdventure.cs.
Edit SuperAdventure.cs, and add these lines to the btnMap_Click function:
using System; using System.ComponentModel; using System.Linq; using System.Windows.Forms; using System.IO; using Engine; namespace SuperAdventure { public partial class SuperAdventure : Form { private const string PLAYER_DATA_FILE_NAME = "PlayerData.xml"; private Player _player; public SuperAdventure() { InitializeComponent(); _player = PlayerDataMapper.CreateFromDatabase(); if(_player == null) { if(File.Exists(PLAYER_DATA_FILE_NAME)) { _player = Player.CreatePlayerFromXmlString(File.ReadAllText(PLAYER_DATA_FILE_NAME)); } else { _player = Player.CreateDefaultPlayer(); } } _player.AddItemToInventory(World.ItemByID(World.ITEM_ID_CLUB)); lblHitPoints.DataBindings.Add("Text", _player, "CurrentHitPoints"); lblGold.DataBindings.Add("Text", _player, "Gold"); lblExperience.DataBindings.Add("Text", _player, "ExperiencePoints"); lblLevel.DataBindings.Add("Text", _player, "Level"); dgvInventory.RowHeadersVisible = false; dgvInventory.AutoGenerateColumns = false; dgvInventory.DataSource = _player.Inventory; dgvInventory.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "Name", Width = 197, DataPropertyName = "Description" }); dgvInventory.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "Quantity", DataPropertyName = "Quantity" }); dgvInventory.ScrollBars = ScrollBars.Vertical; dgvQuests.RowHeadersVisible = false; dgvQuests.AutoGenerateColumns = false; dgvQuests.DataSource = _player.Quests; dgvQuests.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "Name", Width = 197, DataPropertyName = "Name" }); dgvQuests.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "Done?", DataPropertyName = "IsCompleted" }); cboWeapons.DataSource = _player.Weapons; cboWeapons.DisplayMember = "Name"; cboWeapons.ValueMember = "Id"; if(_player.CurrentWeapon != null) { cboWeapons.SelectedItem = _player.CurrentWeapon; } cboWeapons.SelectedIndexChanged += cboWeapons_SelectedIndexChanged; cboPotions.DataSource = _player.Potions; cboPotions.DisplayMember = "Name"; cboPotions.ValueMember = "Id"; _player.PropertyChanged += PlayerOnPropertyChanged; _player.OnMessage += DisplayMessage; _player.MoveTo(_player.CurrentLocation); } 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(); } private void PlayerOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs) { if(propertyChangedEventArgs.PropertyName == "Weapons") { cboWeapons.DataSource = _player.Weapons; if(!_player.Weapons.Any()) { cboWeapons.Visible = false; btnUseWeapon.Visible = false; } } if(propertyChangedEventArgs.PropertyName == "Potions") { cboPotions.DataSource = _player.Potions; if(!_player.Potions.Any()) { cboPotions.Visible = false; btnUsePotion.Visible = false; } } 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); btnTrade.Visible = (_player.CurrentLocation.VendorWorkingHere != null); // Display current location name and description rtbLocation.Text = _player.CurrentLocation.Name + Environment.NewLine; rtbLocation.Text += _player.CurrentLocation.Description + Environment.NewLine; if(!_player.CurrentLocation.HasAMonster) { 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(); } } } 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(); } 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); } private void SuperAdventure_FormClosing(object sender, FormClosingEventArgs e) { File.WriteAllText(PLAYER_DATA_FILE_NAME, _player.ToXmlString()); PlayerDataMapper.SaveToDatabase(_player); } private void cboWeapons_SelectedIndexChanged(object sender, EventArgs e) { _player.CurrentWeapon = (Weapon)cboWeapons.SelectedItem; } private void btnTrade_Click(object sender, EventArgs e) { TradingScreen tradingScreen = new TradingScreen(_player); tradingScreen.StartPosition = FormStartPosition.CenterParent; tradingScreen.ShowDialog(this); } private void btnMap_Click(object sender, EventArgs e) { WorldMap mapScreen = new WorldMap(); mapScreen.StartPosition = FormStartPosition.CenterParent; mapScreen.ShowDialog(this); } } }
Now, you can test the program by running it and clicking on the “Map” button. The new WorldMap window should pop up.
Step 4: Edit SuperAdventure\WorldMap.cs, in Design mode
Now, we will add the images to the map window.
This will be a simple way to display the location images. If you build a bigger world, you might want to change this to only show the locations immediately surrounding the player’s current location.
If you don’t want to manually create all the controls to hold the images, the source code on Gist and Dropbox include the Designer.cs file – which is where all the control information is located. So, you can just copy those over your files, if you use the same name (WorldMap) for your new form.
First, right-click on the WorldMap window and select “Properties”. Set these values:
Text: World Map
Size: 501, 361
MaximizeBox: False
MinimizeBox: False
SizeGripStyle: Hide
We’re going to display the images with PictureBox controls. This control type is in the Toolbox, under the Common Controls.
Left-click on the PictureBox and drag it to the upper-left corner of the WorldMap window. After positioning the PictureBox control, right-click on it and set these properties:
BorderStyle: FixedSingle
SizeMode: StretchImage
(name): pic_0_0
Location: 0, 1
Size: 75, 75
This will draw a border around the location’s image, resize the image to fit the size of the picture box control, and set its size.
Next, you need to add 23 more of these PictureBox controls. We will have four rows and five columns, in total. You want the screen to look like the image below:
You can do this a little faster by placing the first PictureBox, left-clicking on it once, and using Ctrl-C to put it in your clipboard. Then, you can press Ctrl-V on the WorldMap window, to paste in a copy of the PictureBox. Then, drag the new PictureBox to the correct location.
After you’ve placed all 24 PictureBox controls, you’ll need to right-click on each one and give it its name. They’ll all start with “pic” and be followed with an underscore and the row number (starting at the top, with 0 as the first row) and another underscore with the column number (starting at the left, with 0).
When you’re done, they should be named like this:
pic_0_0, pic_0_1, pic_0_2, pic_0_3, pic_0_4, pic_0_5
pic_1_0, pic_1_1, pic_1_2, pic_1_3, pic_1_4, pic_1_5
pic_2_0, pic_2_1, pic_2_2, pic_2_3, pic_2_4, pic_2_5
pic_3_0, pic_3_1, pic_3_2, pic_3_3, pic_3_4, pic_3_5
Step 5: Edit SuperAdventure\WorldMap.cs (the code-behind)
In Solution Explorer, right-click on WorldMap.cs, and select “View Code”.
In this form, we are going to find the image inside the assembly (the program), pull it out of the assembly, format it as a picture, and add the picture to the PictureBox control.
To do all that, we need some additional “using” statements at the top.
- Drawing is for the Bitmap class, which builds the image to display
- IO is to read the bytes of the embedded images, from the assembly
- Reflection is to let us know what assembly we are using
On line 10, we have a new Assembly variable. The assembly is the EXE or DLL that is built when you have Visual Studio built the solution. We set the variable to Assembly.GetExecutingAssembly(), to get the EXE that is running – which is also where the image files are embedded.
Inside the constructor, we call our SetImage function, passing in the PictureBox control we want to fill, and the name of the image file to add to the PictureBox. For the image name, we aren’t including the .png extension, because we do that inside SetImage().
The SetImage function creates a stream, to read bytes from _thisAssembly (the variable we created to point to the running program).
The stream gets the bytes by looking for the resource with the name that matches the string we build on line 30. This value needs to follow the structure of your project’s namespace. So, if you didn’t name your folder “Images” in step 1, you will need to change “.Images.” to the name you used.
If the resource exists, resourceSteam will not be null. So, we takes the bytes from resourceStream and build a new Bitmap object with them, then put that Bitmap object in the PictureBox’s Image property (on line 35).
This is how we get the bytes for the image out of the program and put them on the screen.
It’s a bit of work. But, if you remember to make the image an embedded resource, and use code like the SetImage function, you can add images to to any Windows Form.
WorldMap.cs
using System.Drawing; using System.IO; using System.Reflection; using System.Windows.Forms; namespace SuperAdventure { public partial class WorldMap : Form { readonly Assembly _thisAssembly = Assembly.GetExecutingAssembly(); public WorldMap() { InitializeComponent(); SetImage(pic_0_2, "HerbalistsGarden"); SetImage(pic_1_2, "HerbalistsHut"); SetImage(pic_2_0, "FarmFields"); SetImage(pic_2_1, "Farmhouse"); SetImage(pic_2_2, "TownSquare"); SetImage(pic_2_3, "TownGate"); SetImage(pic_2_4, "Bridge"); SetImage(pic_2_5, "SpiderForest"); SetImage(pic_3_2, "Home"); } private void SetImage(PictureBox pictureBox, string imageName) { using (Stream resourceStream = _thisAssembly.GetManifestResourceStream( _thisAssembly.GetName().Name + ".Images." + imageName + ".png")) { if (resourceStream != null) { pictureBox.Image = new Bitmap(resourceStream); } } } } }
Test that this works by running the program and clicking on the “Map” button. You should see the location images in the World Map window.
Next Lesson
I think that enough for this lesson. In Lesson 26.2 (which I’m starting work on now), I’ll show you how to only show locations that the player has visited. The other locations will be covered with a “fog” image.
Source code for this lesson
NOTE: Because the new WorldMap Form has many PictureBox controls, I’ve included the source code for the WorldMap.Designer.cs file.
Next lesson: Lesson 26.2 – Hiding Unvisited Locations on the World Map
Previous lesson: Lesson 25.1 – Select a random monster at a location
All lessons: Learn C# by Building a Simple RPG Index
Hi!
I think it would be a little more elegant to use the LOCATION_ID constants from the world class, than using the integer numbers directly here:
SetImage(pic_0_2, player.LocationsVisited.Contains(World.LOCATION_ID_ALCHEMISTS_GARDEN) ? “HerbalistsGarden” : “FogLocation”);
SetImage(pic_1_2, player.LocationsVisited.Contains(World.LOCATION_ID_ALCHEMIST_HUT) ? “HerbalistsHut” : “FogLocation”);
SetImage(pic_2_0, player.LocationsVisited.Contains(World.LOCATION_ID_FARM_FIELD) ? “FarmFields” : “FogLocation”);
SetImage(pic_2_1, player.LocationsVisited.Contains(World.LOCATION_ID_FARMHOUSE) ? “Farmhouse” : “FogLocation”);
SetImage(pic_2_2, player.LocationsVisited.Contains(World.LOCATION_ID_TOWN_SQUARE) ? “TownSquare” : “FogLocation”);
SetImage(pic_2_3, player.LocationsVisited.Contains(World.LOCATION_ID_GUARD_POST) ? “TownGate” : “FogLocation”);
SetImage(pic_2_4, player.LocationsVisited.Contains(World.LOCATION_ID_BRIDGE) ? “Bridge” : “FogLocation”);
SetImage(pic_2_5, player.LocationsVisited.Contains(World.LOCATION_ID_SPIDER_FIELD) ? “SpiderForest” : “FogLocation”);
SetImage(pic_3_2, player.LocationsVisited.Contains(World.LOCATION_ID_HOME) ? “Home” : “FogLocation”);
Yes, your version does make the function more obvious.
could you explain this pls, i tried to search this but haven’t really found anything that could really explain some things in your code.:c
1) _player = PlayerDataMapper.CreateFromDatabase(); Where have we created this one ? I have no PlayerDataMapper in my code and i haven’t found in yours.
2) About this moment.
using (Stream resourceStream =
_thisAssembly.GetManifestResourceStream(
_thisAssembly.GetName().Name + “.Images.” + imageName + “.png”))
-Why there are “.Images.” points around it ?
-And why we use + instead of “,” or “=” ?
-I serched about .Name property of GetName and didn’t find
1) The PlayerDataMapper class is from the SQL lesson 22.3. If you skipped over this lesson, and didn’t create this class, you can comment out this line.
2) We are doing “string concatenation” here – adding strings together, to make a longer string. You use “+” to put strings together. For example: “string fullName = firstName + ” ” + middleName + ” ” + lastName;” The string we are building tells the location of the image file, inside the assembly/DLL (the file created when we build a project). Because we added the images to the projects as “embedded resources”, they are part of the DLL. We use “reflection” to get the first part of the assembly name (the _thisAssembly variable on line 10). Then, we need to add the folder that holds the image. This is similar to looking for a file on a disk. You would do that by saying something like: “C:\MyFiles\Images\SpiderForest.png”. But, the path to an embedded resource in an assembly is formatted like: “SuperAdventure.Engine.Images.SpiderForest.png”.
The Name property is from the GetName function of the Assembly class – which we get from calling Assembly.GetExecutingAssembly(). That is part of .NET’s “reflection”. It looks at the assembly/DLL that is running in the program and gets information about it.
You probably don’t need to do much with .NET reflection. It is very advanced, and not commonly used. We just need it for this one thing – finding the image file.