I am wrapping code around MonoGame to announce SokoBomber 2, and have it adapt between platforms. I just figured I want to try something a little different.
For today, since GGJ is nearly here we will make an easier way to split our game’s code up. This is aiming to make it easier for us to work in a team. On the other side, it is also so we can target different platforms with the exact same code.
To some, this may not make any sense. Why would we need to wrap around something like MonoGame? The simplest reason for this is that the team members can work on different files regularly (as in, not a single file containing all states and game code, rather separate files). Using this project structure we can also have all changes to the game’s code working on the other platforms automatically.
We will
For ease, I figured it is a good idea to use an existing project to see how we should do things for this year. That is why this is an introduction to SokoBomber 2. SokoBomber was a game built in Unity years ago, I figured I would like to do something completely different for SokoBomber 2. I have started to create SokoBomber 2‘s procedural level generation, I’m sure you will know that can be a slow (and tedious) task. We currently use the art from the first version, converted from Unity assets manually (so there are flaws in my copy pasting, like white lines around some sprites). This will, once again, be an open source project.
What shall we look at?
Obviously, it is always a good idea to look at the code and consider what we will need.
- Game states
- Input
Yes, a very simple list for what we want to manage for ourselves.
public interface IState
{
void Load(IContentManager _content);
void Update();
void Draw();
}
public interface IContentManager
{
void Load(string _name);
}
public interface ISpriteBatch
{
void Draw(string _what, int _x, int _y, int _width, int _height);
}
As you can no doubt tell, that means the class library needs to be “handed” certain features. This was what I felt sorted the start out, there will be more added to these interfaces soon. We will be constructing the SokoBomber2Engine using the implementation of these interfaces.
public SokoBomber2Engine(ISpriteBatch _spriteBatch, IContentManager _content)
Then, we can make the interfaces now. As I’m sure you know, we can’t show anything yet. How will we be loading the content? Let’s implement
namespace SokoBomber2.Win.EngineInterfaces
{
class EContentManager : IContentManager
{
public void Load(string _name)
{
throw new NotImplementedException();
}
}
}
In here, we will need to use the ContentManager, so we create a constructor and private variable to store the ContentManager in.
public ContentManager Content { get; private set; }
public EContentManager(ContentManager _content)
{
Content = _content;
}
Next, we can make a dictionary to store the sprites in:
namespace SokoBomber2.Win.EngineInterfaces
{
class EContentManager : IContentManager
{
public ContentManager Content { get; private set; }
public EContentManager(ContentManager _content)
{
Content = _content;
Textures = new Dictionary<string, Texture2D>();
}
public Dictionary<string, Texture2D> Textures { get; private set; }
public void Load(string _name)
{
if (!Textures.ContainsKey(_name))
{
Textures.Add(_name, Content.Load<Texture2D>(_name));
}
}
}
}
As you can see, it is super simple. We can now use
private EContentManager ContentManager;
public SpriteBatch SpriteBatch { get; private set; }
private Rectangle DrawRect;
public ESpriteBatch(EContentManager _eContentManager, SpriteBatch _spriteBatch)
{
ContentManager = _eContentManager;
SpriteBatch = _spriteBatch;
DrawRect = new Rectangle();
}
As you can tell, we can create simple properties for us to use. Now, we move over to LoadContent, this is the easiest place to start out. We can add in the initial state, and more.
public SokoBomber2Engine SokoBomber2Engine { get; private set; }
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
var eContentManage = new EContentManager(Content);
var eSpriteBatch = new ESpriteBatch(eContentManage, spriteBatch);
SokoBomber2Engine = new SokoBomber2Engine(eSpriteBatch, eContentManage);
}
This is all of the creation so far. We need to have our first state to move further, for starting out this project it will be very simple starting out.
Create The Menu
The simplest state we need is the menu, for us to create the menu we will first have to load the sprites as images. Within Load we will use the ContentManager to load the sprite (mPause.png), then in Draw, we will draw it in the top left. There is more to load, we just need to make it move a step closer to playable.
_content.Load("mOptions");
Totally too difficult in the Load method. Now, note that we need to adjust the Draw in the states. We add to
// in IState:
void Draw(ISpriteBatch _spriteBatch);
// in StateMenu: Draw becomes
public void Draw(ISpriteBatch _spriteBatch)
{
throw new NotImplementedException();
}
I am sure this is easy to understand? Next we use the SpriteBatch to Draw:
_spriteBatch.Draw("mOptions", 0, 0, 100, 100);
Well, no, not quite. The image isn’t 100×100, that isn’t what we are worrying about. We will need to add a note for ourselves to add a shorter Draw method that uses the size of the sprite. Let’s move over to using the Engine.
public class SokoBomber2Engine
{
public ISpriteBatch SpriteBatch { get; }
public IContentManager Content { get; }
public SokoBomber2Engine(ISpriteBatch _spriteBatch, IContentManager _content)
{
SpriteBatch = _spriteBatch;
Content = _content;
}
public void AddState(IState _state)
{
}
public void Update()
{
}
public void Draw()
{
}
}
We start by mapping what we will need. For simplicity, we will use a List<IState> to track out a list of states. Similarly, since the menu is the first state, when we add the feature in a while to remove the state on top of the stack we won’t ever go below a single state in the list.
private List<IState> States { get; set; }
public void AddState(IState _state)
{
if (States == null) States = new List<IState>();
_state.Load(Content);
States.Add(_state);
}
As you can see, we don’t manage loading screens. That isn’t what we are working on for today’s implementation. The next thing to use is Draw, we will just draw the last state available.
public void Draw()
{
try
{
States[States.Count - 1].Draw(SpriteBatch);
}
catch { }
}
Well, the try/catch may confuse you slightly. We should do that anywhere that we might have a delay. Delays can come from multiple threads trying to access the same code. We don’t need to worry too much, we should just add checks wherever we might experience that.
We now go back to the Windows game code:
public SokoBomber2Engine SokoBomber2Engine { get; private set; }
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
var eContentManage = new EContentManager(Content);
var eSpriteBatch = new ESpriteBatch(eContentManage, spriteBatch);
SokoBomber2Engine = new SokoBomber2Engine(eSpriteBatch, eContentManage);
SokoBomber2Engine.AddState(new StateMenu());
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
SokoBomber2Engine.Update();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
SokoBomber2Engine.Draw();
spriteBatch.End();
base.Draw(gameTime);
}
There are several things we would need to add in the Draw for ourselves. To keep this simple, you will no doubt see all of it on the Github repo. What I did after this can just sit in point form, as it is finishing the menu.
- Added all old Sokobomber art
- Made a simple menu interface that isn’t complete
As you can understand, we now need to move onto the update to get further forward.
Lets Update
Now take note, there are several different ways to look into how we use the input. We will, however, use a minimised implementation. This is so that it will be easy to also use our code again on a separate platform.
KeyboardState PreviousKeyboardState;
KeyboardState _currentKeyboardState;
KeyboardState CurrentKeyboardState {
get {
return _currentKeyboardState;
}
set {
PreviousKeyboardState = _currentKeyboardState;
_currentKeyboardState = value;
}
}
MouseState PreviousMouseState;
MouseState _currentMouseState;
MouseState CurrentMouseState
{
get
{
return _currentMouseState;
}
set
{
PreviousMouseState = _currentMouseState;
_currentMouseState = value;
}
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
CurrentKeyboardState = Keyboard.GetState();
CurrentMouseState = Mouse.GetState();
SokoBomber2Engine.Update();
base.Update(gameTime);
}
This is just so we can make adjustments to the input we keep track of. The implementation can be adjusted over, and over. We are currently just attempting to get the features we want quickly.
//We make an instance we can reference wherever we are in the engine code for ourselves
private static SokoBomber2Engine _instance;
public static SokoBomber2Engine Instance
{
get { return _instance; }
private set { _instance = value; }
}
// We add an instance using the construction
public SokoBomber2Engine(ISpriteBatch _spriteBatch, IContentManager _content)
{
SpriteBatch = _spriteBatch;
Content = _content;
Instance = this;
}
Before continuing, take note that for safety we should probably wrap this everywhere differently. This is just the initial structure that will be adjusted regularly for us. The importance is literally just making sure we can access it when we start out.
We add a wrapping in the Engine, which I know will need to be thought of more. We don’t have to worry yet.
public int MouseX { get; private set; }
public int MouseY { get; private set; }
public bool MouseLeftClicked { get; private set; }
public bool MouseLeftHeld { get; private set; }
public bool MouseRightClicked { get; private set; }
public bool MouseRightHeld { get; private set; }
public void TrackMouse(int _x, int _y, bool _leftClick, bool _previousLeftClick, bool _rightClick, bool _previousRightClick)
{
MouseX = _x;
MouseY = _y;
if (_leftClick)
{
if (!_previousLeftClick)
{
MouseLeftClicked = true;
MouseLeftHeld = false;
}
else
{
MouseLeftHeld = true;
MouseLeftClicked = false;
}
}
else
{
MouseLeftHeld = false;
MouseLeftClicked = false;
}
if (_rightClick)
{
if (!_previousLeftClick)
{
MouseRightClicked = true;
MouseRightHeld = false;
}
else
{
MouseRightHeld = true;
MouseRightClicked = false;
}
}
else
{
MouseRightHeld = false;
MouseRightClicked = false;
}
}
There is a lot that can be done for making this way better, I just wanted to challenge myself to make sure we can be extra fast. Next we update it in the Windows game.
SokoBomber2Engine.TrackMouse(CurrentMouseState.Position.X, CurrentMouseState.Position.Y,
CurrentMouseState.LeftButton == ButtonState.Pressed, PreviousMouseState.LeftButton == ButtonState.Pressed,
CurrentMouseState.RightButton == ButtonState.Pressed, PreviousMouseState.RightButton == ButtonState.Pressed);
SokoBomber2Engine.Update();
There we go, just before we call the update in the Windows game we update the mouse. As you can currently no doubt tell, we will need to do quite a bit more work to use this with every feature we want. I am keeping it minimal as it is warming up for GGJ, this is taking longer as I am writing it all down after I do anything.
Our next step today happens to be making the menu buttons show something when we hover over them.
// First off we add the update in the Engine
public void Update()
{
if (States != null)
{
States[States.Count - 1].Update();
}
}
// Secondly we make a simple Update in Menu
int hoverPlay = 0;
int hoverAbout = 0;
public void Update()
{
var mouseX = SokoBomber2Engine.Instance.MouseX;
var mouseY = SokoBomber2Engine.Instance.MouseY;
if ((mouseX > 604) && (mouseX < 704) &&
(mouseY > 20) && (mouseY < 95))
{
hoverPlay = 2;
}
else
{
hoverPlay = 0;
}
}
// Third we use the int we make in Menu's Draw
_spriteBatch.Draw("mButtonPlay", 604 + hoverPlay, 25 - hoverPlay);
We can now use the simple logic to see when we click Play.
if ((hoverPlay != 0) && (SokoBomber2Engine.Instance.MouseLeftClicked))
{
throw new Exception("We can make the state change!");
}
Sure, this gives us an exception currently. It is only to show how we do things. We will now move onto starting our play state.
Final State Work
Take note, there are tons of things we need to do for this for ourselves. This is just how we can make a state system to wrap around our games. You will note that this is when the project works for changing states. However, we do not have the play implemented yet. Now, what is needed is a way to have a different size level, with blocks in it. Sure, we should also set it up so that it can be playable.
Just note, this may or may not be playable. Also note,
I first sorted out the fact that my Windows update (I suppose, possibly through my motherboard’s config) enabled Hyper-V. I have seen places that say we should disable it, and other places that shared we can use Android with Hyper-V on. I couldn’t, so swapping it over I managed to sort out my Android emulator.
The code literally only needed slight adjustments (for instance, we don’t have a mouse or keyboard in the same way as a PC), and obviously the art. The art brought a flaw in the logic, that is important for you to take note of when you do this for yourself.
_content.Load("mSokobomber");
Notice in the above line (which we had before) I didn’t pay attention to capitalisation. Just remember that Android is Linux based. Under Windows the words “asdf” and “AsDF” are exactly the same in term of file names. Capitalisation isn’t important. This is also something to pay attention to on other systems, but since Android is Linux based we need to have the name capitalised correctly. This was the replacement to fix it:
_content.Load("mSokoBomber");
Since the file name has a capital “B” we need to load it with a capital. The same thing for the draw. The result is simple to understand:
You can no doubt tell that this isn’t quite what we want to show, it would need us to adjust the art layout for ourselves. We will add it to how the wrapper works. Now take note, there are several ways we can do this. I figure we will do it in a slightly strange minimal way.
We will make the instance of the Engine use a specific set of data for output. Firstly, we make an enum and instantiate the engine according to the value.
public enum EngineType
{
PC,
Android
}
Note that, usually, we would make a
public SokoBomber2Engine(ISpriteBatch _spriteBatch, IContentManager _content, EngineType _engineType)
{
SpriteBatch = _spriteBatch;
Content = _content;
Instance = this;
EngineType = _engineType;
GenerateProperties();
}
private void GenerateProperties()
{
throw new NotImplementedException();
}
We can just make it use whatever we need to use in GenerateProperties. Thinking about it while I type it out for the post, I probably should have done something different I figure.
public Dictionary<string, int> PropertyValues;
private void GenerateProperties()
{
PropertyValues = new Dictionary<string, int>();
switch (EngineType)
{
case EngineType.Android:
case EngineType.PC:
break;
}
}
We can take the values that we want to adjust in the Draw from StateMenu and initiate them here. Take note of the following:
public void Draw(ISpriteBatch _spriteBatch)
{
_spriteBatch.Draw("mSokoBomber", 0, 0);
_spriteBatch.Draw("mOptions", 550, 0);
_spriteBatch.Draw("mButtonPlay", 604 + hoverPlay, 25 - hoverPlay);
_spriteBatch.Draw("mButtonAbout", 604 + hoverAbout, 105 - hoverAbout);
_spriteBatch.Draw("mTwo", 165, 370);
_spriteBatch.Draw("mouse", SokoBomber2Engine.Instance.MouseX, SokoBomber2Engine.Instance.MouseY);
}
Each integer value in there (as well as width, and height) should have adjustments for Android, we will however just make the PC’s version for everything. Sure, this adds issues in terms of simplicity, we just don’t need to be worried about that. We can optimise things late in the project.
The next steps will add some delays. They are definitely easy to understand, adding the input using the touch screen on an Android device. Also note, this will be tested over and over on my PC currently in the Android emulator, then when it starts to be a playable game I will see how it looks and feels on my phone. I know the Menu’s options on the top right are most likely way too small.
Get Android Input
First, we will make sure that the TouchPanel has capabilities. If it doesn’t our Android testing won’t work out. You will be able to see better descriptive detail, for what I am currently doing we will stick to keeping it minimal.
if (!TouchPanel.GetCapabilities().IsConnected)
throw new System.Exception("TouchPanel won't work.");
When
First, I needed to remember to move the buttons.
if ((mouseX > SokoBomber2Engine.Instance.PropertyValues["mButtonPlay.X"]) && (mouseX < SokoBomber2Engine.Instance.PropertyValues["mButtonPlay.X"] + 100) &&
(mouseY > SokoBomber2Engine.Instance.PropertyValues["mButtonPlay.Y"]) && (mouseY < SokoBomber2Engine.Instance.PropertyValues["mButtonPlay.Y"] + 60))
{
hoverPlay = 2;
}
else
{
hoverPlay = 0;
}
As you can see, this method may seem like it adds too much. It definitely does, however, this way it is just easy to constantly change the values we use. We will likely set up two enums replacing this dictionary. This is just understandable for debugging, when we have a value incorrect we can easily change it while code is running. Eventually we will use static constant values, that isn’t what we need at this point in the project.
Similarly, you will see in the Update that this is yet another minimal option.
bool leftClicked = false;
int x = 0, y = 0;
TouchCollection tCollection = TouchPanel.GetState();
foreach (var tLocation in tCollection)
{
if (tLocation.State == TouchLocationState.Released)
{
leftClicked = true;
x = (int)Math.Round(tLocation.Position.X);
y = (int)Math.Round(tLocation.Position.Y);
}
break;
}
SokoBomber2Engine.TrackMouse(x, y,
leftClicked, false,
false, false);
We only trust the touch panel from the first point on the touchscreen until that is gone. This can have several issues when playing the game on our phones in the future, we just won’t worry about “bugs” at the moment.
The next thing we will add to finish this off is simple, it still isn’t making SokoBomber 2 completely playable. We want to add the movement code in, essentially within the Play state, we need to set up using the Mouse (where we bind the touch screen capability to the Engine) to move.
We will head into the StatePlay, the Update method will have a bit added to it. To get that to work we will the engine to know the size of the screen.
public SokoBomber2Engine(ISpriteBatch _spriteBatch, IContentManager _content, EngineType _engineType, int _screenWidth, int _screenHeight)
{
// ... Code not shown here stayed the same ...
// This is added:
ScreenWidth = _screenWidth;
ScreenHeight = _screenHeight;
}
Then obviously in each place we create the Engine we add it in giving the values:
SokoBomber2Engine = new SokoBomber2Engine(eSpriteBatch, eContentManage, EngineType.Android, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height);
The maths we will use here is slightly simple, and not necessarily enough. We just want to start with making the feature work for ourselves. We will take half the screen width, and height, away from the “mouse position”. We will then work out the direction that is from the point 0,0.
// Add Mouse Here
if (SokoBomber2Engine.Instance.MouseLeftClicked)
{
int posX = SokoBomber2Engine.Instance.MouseX - (SokoBomber2Engine.Instance.ScreenWidth / 2);
int posY = SokoBomber2Engine.Instance.MouseY - (SokoBomber2Engine.Instance.ScreenHeight / 2);
var angle = Math.Atan2(posY, posX) * 180.0 / Math.PI;
// Up
if ((angle >= -90 -45) && (angle < -45))
{
playerTileY--;
}
// Right
else if ((angle >= -45) && (angle < 45))
{
playerTileX++;
}
// Down
else if ((angle >= 45) && (angle < 90 + 45))
{
playerTileY++;
}
// Left
else
{
playerTileX--;
}
}
You can no doubt tell, this is quite simple. It is easy to understand, it also has features we don’t necessarily like. One of those is is the fact that it doesn’t pay attention to certain percentages of the touch screen. I will be swapping it to input on a “key pad” I make at the bottom in the future, we just aren’t worrying about that today.
This does not have much in terms of the game mechanics, but you can see from the gifs below that it does the same thing on both systems (phone gif low quality as I figured it didn’t matter too much).
As you can see above, the code works on both. This is simply so we can use the exact same game code on separate systems.
Summary for GGJ
I feel this might not make too much sense yet. Sure, apologies in order for just showing that SokoBomber 2 is properly in the works now.
This is the way that you can wrap around your game’s code to use the same code on completely different systems. We are making an Android game? Cool, we wrapped game code in a way that we can have it run easily on Android. No, wait, I have an iPhone…? Well, take a look:
Note that this is using MonoGame through Visual Studio 2017, you don’t need to. This just happens to be where I decided to show it off for myself. Through others, which we talked about before (like GMS and Unity), they have the structure built into them to “wrap” sections of the game together. I mean, take a look at it: we put objects into rooms. This is similar for us, just using C# as the engine. We could likely also use other engines instead of MonoGame with this wrap. That is why it is minimal in terms of what is needed for features within the game.
This is what I feel like about this year’s GGJ, I would love to join a team that wouldn’t mind doing this for the game we create. We can then target several platforms at the same time with the same code (more, or less). I figure that would be a fun challenge I would love to have this year.