![]() |
#1
|
||||
|
||||
![]()
I've been wanting to make a game making tutorial because most of the ones that were around when I first started weren't great so...
Originally I had written this long post going into setting up your development environment and all that but I decided it might be better to talk about a game we're all probably familiar with and then we'll go about cloning it (sorta). Game Maker and Unity are supposed to be really good for beginners to use but I don't have much experience in them. I do have a little experience in C# (the programming language) and XNA (the game framework) so I'm going to use those. The downside to this is that XNA isn't an engine (like Unity) so we'll have to roll our own, the upside is that we aren't go to do something incredibly complex. Another downside to XNA is that it's kinda dead (as in Microsoft no longer supports it) but it's still usable thanks to FNA and Monogame (don't worry about those for now). So let's talk about Space Invaders: ![]() For starters, let's just list everything we see: Code:
The word "SCORE<1>" The word "HI-SCORE" The word "SCORE<2>" The player 1 score (4 digits long) The high score (4 digits long) The player 2 score (4 digits long) 55 aliens 4 shields 1 player The number of lives left (3 currently) 2 ship icons The word "CREDIT" The number of credits (2 digits long) BONUS: The UFO that shows up periodically Bullets the aliens shoot Bullets the player shoots ![]() The data type indicates how much memory you'll use, I picked byte because it has a range of 0 to 255 (like rupees in the Legend of Zelda!) and you generally want to pick the smallest data type you can to conserve memory. Fun fact: I only used int, float, string, and bool for everything I did in college and a good portion of my career and no one complained, lol. So anyway, let's look at our list of stuff on the screen and pair it up with a data type (as best we can). https://msdn.microsoft.com/en-us/lib...(v=vs.90).aspx Code:
The word "SCORE<1>" - string The word "HI-SCORE" - string The word "SCORE<2>" - string The player 1 (4 digits long) - uint or string The high score (4 digits long) - uint or string The player 2 (4 digits long) - uint or string 55 aliens - nothing fits :( 4 shields - nothing fits :( 1 player - nothing fits :( The number of lives left (3 currently) - byte or string 2 ship icons - nothing fits :( The word "CREDIT" - string The number of credits (2 digits long) - byte or string BONUS: The UFO that shows up periodically - nothing fits :( Bullets the aliens shoot - nothing fits :( Bullets the player shoots - nothing fits :( Code:
The word "SCORE<1>" - string The word "HI-SCORE" - string The word "SCORE<2>" - string The player 1 (4 digits long) - string The high score (4 digits long) - string The player 2 (4 digits long) - string 55 aliens - Texture2D 4 shields - Texture2D 1 player - Texture2D The number of lives left (3 currently) - string 2 ship icons - Texture2D The word "CREDIT" - string The number of credits (2 digits long) - string BONUS: The UFO that shows up periodically - Texture2D Bullets the aliens shoot - Texture2D Bullets the player shoots - Texture2D Code:
string score1Lbl; string hiScoreLbl; string score2Lbl; string player1Score; string highScore; string player2Score; Texture2D alien; Texture2D shield; Texture2D player; string numLives; Texture2D shipLifeIcon; string creditLbl; string numCredit; Texture2D ufo; Texture2D alienBullet; Texture2D playerBullet; // this is declaring the variable age and assigning it the value 25 byte age = 25; // this is assigning to a variable that is known (and is in scope but we'll talk scope later) age = 26; so let's assign where we can: Code:
string score1Lbl = "SCORE<1>"; string hiScoreLbl = "HI-SCORE"; string score2Lbl = "SCORE<2>"; string player1Score = "0000"; string highScore = "0000"; string player2Score = "0000"; Texture2D alien; Texture2D shield; Texture2D player; string numLives = "3"; Texture2D shipLifeIcon; string creditLbl = "CREDIT"; string numCredit = "00"; Texture2D ufo; Texture2D alienBullet; Texture2D playerBullet; So if we pasted in the current code into our program and ran it, it would run but it wouldn't display anything yet because we haven't specified where we want everything displayed, in addition to the fact that we haven't told the program to display anything. The first step in doing this is coming up with variables for the location of everything we want to display. So for laughs, I'm going to draw a box around each individual thing we are going to display. ![]() Now that I've done that I'm going to briefly go off on a tangent about XNA's 2D coordinate system. In XNA, the upper left corner of the screen is coordinate x:0, y:0. The bottom right corner of the screen is coordinate x:width of the screen, y: height of the screen. So if you're moving right, the x value is going up and left the x value is going down. Similarly, if you are going up, the y is doing down and down the y value is going up. We could use individual int values for the x and y value like: int scorePositionX = 50; int scorePositionY = 95: We /could/ do that but instead we are going to use a built in data type in XNA called a Vector2. A Vector2 holds an X and a Y value and some other junk we're not going to worry about right now. Initializing a Vector2 looks like this: ![]() There are two new things here that we haven't seen before: the "new" keyword and the "constructor". The "new" keyword is used to create new objects. The constructor specifies what kind of object is being created and with which values. The Vector2 structure has several constructors we could use, they look like this: // this is the default constructor, it will assign x and y to zero Vector2() // this is a constructor that will set x and y to the value passed in (I've never used this one and had to google it) Vector2(float value) // this is a constructor that will set x to the first value and y to the second value passed in Vector2(float x, float y) I'm going to use the last constructor to set all the locations to 0,0 for now. Code:
string score1Lbl = "SCORE<1>"; string hiScoreLbl = "HI-SCORE"; string score2Lbl = "SCORE<2>"; string player1Score = "0000"; string highScore = "0000"; string player2Score = "0000"; Texture2D alien; Texture2D shield; Texture2D player; string numLives = "3"; Texture2D shipLifeIcon; string creditLbl = "CREDIT"; string numCredit = "00"; Texture2D ufo; Texture2D alienBullet; Texture2D playerBullet; Vector2 score1Pos = new Vector2(0, 0); Vector2 hiScorePos = new Vector2(0, 0); Vector2 score2Pos = new Vector2(0, 0); Vector2 player1ScorePos = new Vector2(0, 0); Vector2 highScorePos = new Vector2(0, 0); Vector2 player2ScorePos = new Vector2(0, 0); Vector2 alienPos = new Vector2(0, 0); Vector2 shieldPos = new Vector2(0, 0); Vector2 playerPos = new Vector2(0, 0); Vector2 numLivesPos = new Vector2(0, 0); Vector2 shipLifeIconPos = new Vector2(0, 0); Vector2 creditPos = new Vector2(0, 0); Vector2 numCreditPos = new Vector2(0, 0); Vector2 ufoPos = new Vector2(0, 0); Vector2 alienBulletPos = new Vector2(0, 0); Vector2 playerBulletPos = new Vector2(0, 0); Code:
public class Sprite { Vector2 position; Texture2D image; // constructor public Sprite(Vector2 p, Texture2D i) { position = p; image = i; } } Code:
public class DisplayText { Vector2 position; string text; // constructor public DisplayText(Vector2 p, string t) { position = p; text = t; } } Code:
DisplayText score1Lbl = new DisplayText(new Vector2(0,0), "SCORE<1>"); DisplayText hiScoreLbl = new DisplayText(new Vector2(0,0), "HI-SCORE"); DisplayText score2Lbl = new DisplayText(new Vector2(0,0), "SCORE<2>"); DisplayText player1Score = new DisplayText(new Vector2(0,0), "0000"); DisplayText highScore = new DisplayText(new Vector2(0,0), "0000"); DisplayText player2Score = new DisplayText(new Vector2(0,0), "0000"); Sprite alien = new Sprite(new Vector2(0,0), ????); Sprite shield = new Sprite(new Vector2(0,0), ????); Sprite player = new Sprite(new Vector2(0,0), ????); DisplayText numLives = new DisplayText(new Vector2(0,0), "3"); Sprite shipLifeIcon = new Sprite(new Vector2(0,0), ????); DisplayText creditLbl = new DisplayText(new Vector2(0,0), "CREDIT"); DisplayText numCredit = new DisplayText(new Vector2(0,0), "00"); Sprite ufo = new Sprite(new Vector2(0,0), ????); Sprite alienBullet = new Sprite(new Vector2(0,0), ????); Sprite playerBullet = new Sprite(new Vector2(0,0), ????); Part 2 coming ???? Last edited by Balrog; 02-20-2016 at 12:22 AM. |
#2
|
||||
|
||||
![]()
Thanks for this. I'll be following along when I get home from MAGfest.
|
#3
|
||||
|
||||
![]()
This is a cool tutorial and a good idea.
Couple questions.. does XNA code port directly over to Monogame? I have an old XNA 4.0 book, and ran through a few chapters of, but stopped after I heard the library was no longer supported. C# is a really solid language though, so I'd be into figuring out how to work with the library. Maybe you'd want to cover how to set up an XNA/Monogame project or something...? |
#4
|
||||
|
||||
![]()
I'm totally down for this sort of topic in general, though since I have, uh, a couple decades of C++ development under my belt what you're covering here is stuff I'm familiar with. But it looks like a good intro for people who aren't! I assume if/when I get into actually mucking about with game design I'll be looking into Unity, but you can't go wrong with a grounding in the basics.
Quote:
|
#5
|
||||
|
||||
![]()
woot!
|
#6
|
||||
|
||||
![]()
Oh HELL yeah!
|
#7
|
||||
|
||||
![]() Quote:
![]() |
#8
|
||||
|
||||
![]()
Huh, checked out the FNA page and that "single build process of Windows, OSX, and Linux" thing along with all-free-OSS implementation sounds quite attractive. Of course the drawback since they focus on open platforms is it looks like there's no easy port to iOS and Android. Hmm.
Of course, depending on what I decide to do, I may want Unity's engines to start with anyway. |
#9
|
|||
|
|||
![]()
This is cool! As someone who writes code all day to make businesses run, I'm excited to see code that draws pretty pictures and responds to user input in real-time.
But I'm confused by something: in the last code section player1Score is constructed as "new DisplayText()", but player2Score is constructed as "new UI()". Is there a difference? Also, at risk of being nit-picky, having two vars named "hiScore" and "highScore" might cause some confusion down the line. How about something like "hiScoreLabel" and "hiScoreVal" instead? |
#10
|
||||
|
||||
![]()
I bet UI() a copy-paste error from a later revision, with a new structure Balrog will introduce for the bits of text that change in response to the game state as opposed to the static strings.
|
#11
|
||||
|
||||
![]()
Holy piss this is amazing. Thank you so much, you've explained things so clearly and helpfully too. I'm a learner programmer so seeing how to program something I actually give a damn about (muh gaemz) is really helpful and intuitive for me.
|
#12
|
||||
|
||||
![]() Quote:
|
#13
|
||||
|
||||
![]() Quote:
![]() Quote:
|
#14
|
||||
|
||||
![]()
Honestly I'm not sure if the particular games I have in my head right now would be workable on mobile either. But I feel like if I'm learning new stuff anyway it would be most helpful to learn something that at least has a viable path to iOS/Android, since being able to do stuff there seems like such a generally useful skill in the current gaming aand general programming environment.
|
#15
|
||||
|
||||
![]()
The next thing that we need to do is install XNA (https://www.microsoft.com/en-us/down....aspx?id=23714) DirectX 9c (https://www.microsoft.com/en-us/down....aspx?id=34429) and some flavor of Visual Studio Express, I'm still using 2010 (http://download.microsoft.com/downlo...10Express1.iso) but I've heard that the newest one works with XNA with some modifications (info here: http://stackoverflow.com/questions/2...o-2015-preview).
Once you've installed all of that and restarted you computer, you should be ready to go. So to start, once you've installed Visual Studio, open the program, click "New Project...", select "XNA Game Studio 4.0" from the left hand options (it's under Visual C#), select "Windows Game (4.0)" from the middle options, and name the game TalkingInvaders in the name field at the bottom and click the OK button. It should look something like this: ![]() Visual Studio has created the program and given us some files to start off with. You can run the program now to see what it does by pressing the "F5" key or by selecting Debug->Start Debugging from the top menu. It should look like this: ![]() So it doesn't look much like a game, in fact it doesn't look like much at all but there's a lot of stuff going on underneath that we sorta need to understand to move forward so I'll attempt to explain that now. Open up the Game1.cs file on the right side and I'll go over the important stuff as best I can. Game1.cs has the class definition (think of it as a blueprint) for our game. Here's the highlights: public Game1() Game1 is the default name for the game, and when the Game1() method (or function if you want to call it that) is called inside the Program.cs file, it creates an instance of our game. If you look at the function, it says graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; ...we're not going to worry about those now. https://msdn.microsoft.com/en-us/lib...nitialize.aspx protected override void Initialize() This function will be called once after the Game1() constructor gets called. We can initialize variables (kinda like the graphics = new GraphicsDeviceManager(this); thing above). Generally this is used to initialize non-graphic variables. https://msdn.microsoft.com/en-us/lib...adcontent.aspx protected override void LoadContent() You've seen loading screens before, right? That's what this function does. It loads graphics, music, sounds, etc. into memory. Since most of the stuff we'll create relies on graphics later on, I'll initialize some variables here. This gets called only once (usually). https://msdn.microsoft.com/en-us/lib...adcontent.aspx protected override void UnloadContent() This does the opposite of LoadContent. That funcion loads things into memory and this one unloads it. This will get called once when the game ends. https://msdn.microsoft.com/en-us/lib...me.update.aspx protected override void Update(GameTime gameTime) There's a boolean variable that's set behind the scenes called "IsFixedTimeStep", it's true by default and it causes the game to call the Update method every 16 milliseconds. This method will eventually handle our user input, moving things around, updating animations, shooting bullets, etc. If you manually set " IsFixedTimeStep" to false Update and Draw will get called sequentially as often as possible. I've never set it to false so I don't know much about it. https://msdn.microsoft.com/en-us/lib...game.draw.aspx protected override void Draw(GameTime gameTime) This method renders things to the screen. It will get called as often as it can (when IsFixedTimeStep is set to true). If the update method is taking too long to run, things will look laggy and crappy. So to recap, Game1(), Initialize(), LoadContent(), UnloadContent() get called only once currently where Update gets called every 16 milliseconds, and Draw gets called as quickly as it can. Also, if your Update takes too long to run, your Draw won't have time to run and the game will start stuttering and look crappy. ![]() The next thing we're going to do is start hacking some basic details into place so we can start positioning things correctly. Some light googling tells me that the resolution for Space Invaders is 224x256. That's pretty small by modern standards but I'm going to go with it. We're going to set those values in the Game1() method inside of the Game1.cs file. Our Game1 method should look like this: Code:
public Game1() { graphics = new GraphicsDeviceManager(this); graphics.PreferredBackBufferWidth = 224; graphics.PreferredBackBufferHeight = 256; Content.RootDirectory = "Content"; } ![]() It's so tiny and precious. We'll scale the whole thing up later but for now let's leave it. Next let's change the background color. If you look in the Draw method you'll see this: Code:
GraphicsDevice.Clear(Color.CornflowerBlue); Code:
GraphicsDevice.Clear(Color.Black); Code:
GraphicsDevice.Clear(new Color(0,0,0)); Next, let's paste in the code we wrote in the last tutorial. Let's start with the two classes, Sprite and DisplayText. In the right menu, right click on TalkingInvaders (the one with the Windows symbol next to it) and then Add and then Class. It should default to Class in the middle, type in "Sprite.cs" in the bottom textbox and click the Add button. Replace the class Sprite and the two curly braces below with this: Code:
public class Sprite { Vector2 position; Texture2D image; // constructor public Sprite(Vector2 p, Texture2D i) { position = p; image = i; } } Code:
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; Repeat the same steps for the DisplayText class we wrote in the last tutorial. After that you should be able to hit F5 and run it again. If nothing pops up, you've got errors we'll have to figure out ![]() The next thing we're going to do is paste in our variables from the first tutorial. Where you declare a variable is what determines how long it lives and who can access it. Consider the following: Code:
public class Person { public byte getAge() { int age = 0; age = age + 1; return age; } } Code:
public class Person { int age = 0; public byte getAge() { age = age + 1; return age; } } Here's some more info about scope and lifetime: https://msdn.microsoft.com/en-us/library/awt60xs1.aspx Also, a crappy diagram: ![]() For now I'm just going to put them inside of the Game1.cs file, at the top right under "SpriteBatch spriteBatch;", paste in the code. It should look like this: ![]() I might move a few of these around later but if we put them here, we can access them in the Update and Draw methods so it's just easier for now. There's errors because I put in those ???? as placeholders. For now, let's comment those out so it will compile. To comment something out either put a "//" in front of it or wrap it like this /* this is a comment */. Your code should look like this: ![]() We can hit F5 at this point to run it but it's not going to render anything because we haven't told it to render anything yet. I'm going to tackle rendering the text first, then I'll move to rendering images. To render text in XNA you need a string (which we have) and a SpriteFont. A SpriteFont is just a font that's imported into your project. Get this font here: http://www.fonts2u.com/space-invaders-regular.font and unzip it wherever, then right click on the space_invaders.ttf file and click Install. It sucks but Visual Studio only looks for new fonts when it starts so you'll have to shut down Visual Studio and restart it now. Open the TalkingInvaders project back up. Now right click on TalkingInvadersContent (Content) on the right side, go to Add, then New Item, click on Sprite Font in the middle, and change the Name at the bottom to SpaceFont.spritefont and click the Add button in the bottom right. It should automatically add and open up the SpaceFont.spritefont file. This file is an xml format file that has some metadata about the font. Inside the <FontName> replace the "Segoe UI Mono" with "Space Invaders", change the size to 6, the Spacing to 3, and UseKerning to false. FUN FACT: I just guessed on that font size, spacing, and kerning by eyeballing a screenshot. It's probably not too accurate. The file should look like this: ![]() At this point the font is in our project but it's not in the game yet because we haven't loaded it yet. To do this, we'll need to declare a SpriteFont variable and load the font. Let's just declare a new SpriteFont variable called "font" like this: Code:
SpriteFont font; Code:
font = Content.Load<SpriteFont>("SpaceFont"); Code:
spriteBatch.Begin(); spriteBatch.DrawString(font, "TEST", new Vector2(0,0), Color.White); spriteBatch.End(); You can press F5 to test this out. It should look like this: ![]() Since our test string worked, I want to start working in the real strings we're going to use. Inside the DisplayText class there's two variables, position and text. By default, variables inside classes are private and we can't access them directly but we can access them through public methods (we could also make the variables public but that's frowned upon, there's also another way to access private variables but I'll go into that later). So we're going to write some methods (or functions) to access the text and position variables. Code:
public class DisplayText { Vector2 position; string text; // constructor public DisplayText(Vector2 p, string t) { position = p; text = t; } public string getText() { return text; } public Vector2 getPosition() { return position; } } Code:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(new Color(0,0,0)); spriteBatch.Begin(); spriteBatch.DrawString(font, score1Lbl.getText(), score1Lbl.getPosition(), Color.White); spriteBatch.End(); // TODO: Add your drawing code here base.Draw(gameTime); } Code:
spriteBatch.DrawString(font, score1Lbl.getText(), score1Lbl.getPosition(), Color.White); spriteBatch.DrawString(font, hiScoreLbl.getText(), hiScoreLbl.getPosition(), Color.White); spriteBatch.DrawString(font, score2Lbl.getText(), score2Lbl.getPosition(), Color.White); spriteBatch.DrawString(font, player1Score.getText(), player1Score.getPosition(), Color.White); spriteBatch.DrawString(font, highScore.getText(), highScore.getPosition(), Color.White); spriteBatch.DrawString(font, player2Score.getText(), player2Score.getPosition(), Color.White); spriteBatch.DrawString(font, numLives.getText(), numLives.getPosition(), Color.White); spriteBatch.DrawString(font, creditLbl.getText(), creditLbl.getPosition(), Color.White); spriteBatch.DrawString(font, numCredit.getText(), numCredit.getPosition(), Color.White); Code:
DisplayText score1Lbl = new DisplayText(new Vector2(6,1), "SCORE<1>"); DisplayText hiScoreLbl = new DisplayText(new Vector2(79,1), "HI-SCORE"); DisplayText score2Lbl = new DisplayText(new Vector2(151,1), "SCORE<2>"); DisplayText player1Score = new DisplayText(new Vector2(23,16), "0000"); DisplayText highScore = new DisplayText(new Vector2(87,16), "0000"); DisplayText player2Score = new DisplayText(new Vector2(160,16), "0000"); //Sprite alien = new Sprite(new Vector2(0,0), ????); //Sprite shield = new Sprite(new Vector2(0,0), ????); //Sprite player = new Sprite(new Vector2(0,0), ????); DisplayText numLives = new DisplayText(new Vector2(7,232), "3"); //Sprite shipLifeIcon = new Sprite(new Vector2(0,0), ????); DisplayText creditLbl = new DisplayText(new Vector2(135,232), "CREDIT"); DisplayText numCredit = new DisplayText(new Vector2(191,232), "00"); //Sprite ufo = new Sprite(new Vector2(0,0), ????); //Sprite alienBullet = new Sprite(new Vector2(0,0), ????); //Sprite playerBullet = new Sprite(new Vector2(0,0), ????); ![]() End of Round 2! |
#16
|
||||
|
||||
![]() ![]() Yay I'm learning! Thanks again, Balrog |
#17
|
||||
|
||||
![]()
That's awesome! High fives!
|
#18
|
||||
|
||||
![]()
Really awesome tutorial. Thanks for doing this, following along and enjoying myself so far.
One issue I ran into: when rendering my SpriteFont, it wound up looking incredibly blurry and compressed. I figured this was due to some graphics driver issue or something, since I was running Visual Studio in a bootcamp partition on my Mac. I migrated everything on to my desktop PC, which took a few hours since I had to install Visual Studio/XNA etc., and much to my dismay the issue was still happening. I eventually resolved it. I think the issue might have something to do with my environment (XNA 4.0.4, DirectX 11.3, Visual Studio 2013 Pro). I read that somewhere down the line, XNA decided to render their SpriteFonts in a different way that involves compressing them before they are rendered. This leads to a blurry/sloppy/jaggy looking font, rather than the crisp, clean, pixelated look this tutorial showcases. The solution was a framework called Nuclex -- specifically, the SpriteFont portion of the framework (installation instructions here). Once I added that reference to my Content package, and then updated the Content Processor in the properties of my spritefont file, the font rendered perfectly. Keep this tutorial going, really liking it so far. |
#19
|
||||
|
||||
![]()
I've had the blurry text issue with Monogame before but not with XNA. I didn't know how portable Nuclex was so I used this font converter found here: http://blogs.msdn.com/b/shawnhar/arc...ts-in-xna.aspx to use bitmap fonts.
|
#20
|
||||
|
||||
![]()
The next thing we're going to do it get images up on the screen. The process is going to be sorta similar to how we did the text. I'll start with the player's ship. Download the file here:
https://dl.dropboxusercontent.com/u/...orial/Ship.png Inside the project, right click on TalkingInvadersContent (Content) on the right hand side, then Add, then Existing item and browse for the Ship.png you just downloaded. For laughs, click on the Ship.png that it added. It should look something like this: ![]() I'll briefly go over this becuase it gave me heartburn because I glazed over it when I was first learning XNA. The Asset Name is the name you'll reference this content with, it strips out the .png so if you have two files like Ship.png and Ship.jpg, it'll complain. The Build Action is set to Compile which is weird. Our code gets compiled into an executable but it kinda seems weird to do that for graphics/music/etc. XNA will compile those things into another format that it can use. Content Importer is set to texture since this is a png but it will try to autodetect what you've got but it's not always right, the same goes for Content Processor. For example in my other games I have a font that comes from a .bmp file, XNA will try to use the Texture Content Processor but I have to change it to Sprite Font Texture. Copy to Output Directory says whether or not the file will be copied to the /bin folder with the executable. I change that to always copy for level files and stuff that I didn't bother to write a custom Content Processor for D: The "Ship.png" is going to be a Texture2D like I talked about in the first tutorial. So let's declare a Texture2D variable called shipSprite in the Game1.cs file towards the top under "SpriteFont font;". It should look like this: Code:
Texture2D shipSprite; Code:
shipSprite = Content.Load<Texture2D>("Ship"); The Color option at the end seems kinda unneccessary and weird but for for now just pretend it's the color of light shining on the sprite. If you change it to red or yellow, it'll take on a red or yellow tint. If you pick black, the sprite won't show up at all (on a black background). FUN FACT: You can also play with the transparency of the sprite by making a custom color like Color(red, green, blue, alpha transparency). Your draw method should look like this now: Code:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(new Color(0,0,0)); spriteBatch.Begin(); // render UI spriteBatch.DrawString(font, score1Lbl.getText(), score1Lbl.getPosition(), Color.White); spriteBatch.DrawString(font, hiScoreLbl.getText(), hiScoreLbl.getPosition(), Color.White); spriteBatch.DrawString(font, score2Lbl.getText(), score2Lbl.getPosition(), Color.White); spriteBatch.DrawString(font, player1Score.getText(), player1Score.getPosition(), Color.White); spriteBatch.DrawString(font, highScore.getText(), highScore.getPosition(), Color.White); spriteBatch.DrawString(font, player2Score.getText(), player2Score.getPosition(), Color.White); spriteBatch.DrawString(font, numLives.getText(), numLives.getPosition(), Color.White); spriteBatch.DrawString(font, creditLbl.getText(), creditLbl.getPosition(), Color.White); spriteBatch.DrawString(font, numCredit.getText(), numCredit.getPosition(), Color.White); // render player spriteBatch.Draw(shipSprite, new Vector2(50, 50), Color.White); spriteBatch.End(); base.Draw(gameTime); } ![]() Now that we've tested it out, let's expose the variables inside the Sprite class like we did with the DisplayText class. Place these two functions in the Sprite class. These two functions will return the variables that we passed in with the Sprite's constructor. Code:
public Texture2D getImage() { return image; } public Vector2 getPosition() { return position; } Code:
DisplayText score1Lbl = new DisplayText(new Vector2(6,1), "SCORE<1>"); DisplayText hiScoreLbl = new DisplayText(new Vector2(79,1), "HI-SCORE"); DisplayText score2Lbl = new DisplayText(new Vector2(151,1), "SCORE<2>"); DisplayText player1Score = new DisplayText(new Vector2(23,16), "0000"); DisplayText highScore = new DisplayText(new Vector2(87,16), "0000"); DisplayText player2Score = new DisplayText(new Vector2(160,16), "0000"); DisplayText numLives = new DisplayText(new Vector2(7,232), "3"); DisplayText creditLbl = new DisplayText(new Vector2(135,232), "CREDIT"); DisplayText numCredit = new DisplayText(new Vector2(191,232), "00"); Sprite player; Sprite alien; Sprite shield; Sprite shipLifeIcon; Sprite ufo; Sprite alienBullet; Sprite playerBullet; Our LoadContent should look like this now (I set the position to 103, 208 by looking at a screenshot, it may not be accurate): Code:
protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); font = Content.Load<SpriteFont>("SpaceFont"); shipSprite = Content.Load<Texture2D>("Ship"); player = new Sprite(new Vector2(103, 208), shipSprite); // alien = new Sprite(new Vector2(0,0), ????); // shield = new Sprite(new Vector2(0,0), ????); // shipLifeIcon = new Sprite(new Vector2(0,0), ????); // ufo = new Sprite(new Vector2(0,0), ????); // alienBullet = new Sprite(new Vector2(0,0), ????); // playerBullet = new Sprite(new Vector2(0,0), ????); } Code:
spriteBatch.Draw(shipSprite, new Vector2(50, 50), Color.White); Code:
spriteBatch.Draw(player.getImage(), player.getPosition(), Color.White); Code:
KeyboardState keyboardState = Keyboard.GetState(); Code:
if (keyboardState.IsKeyDown(Keys.Left)) { } else if (keyboardState.IsKeyDown(Keys.Right)) { } else { } if (keyboardState.IsKeyDown(Keys.Space)) { } In Space Invaders, the player can only move left or right and they're stopped on both sides (about the width of the ship from the side of the screen). First we'll code the moving around, then the stopping at both sides. Unfortunately, at this point, we can get the position of a sprite but we can't set it (outside of the constructor). We'll have to write a quick function to do that. Since the player can only move right to left, let's just write a function that does that. Place this in your Sprite.cs file: Code:
public void moveXAxis(float distance) { position.X = position.X + distance; } Code:
if (keyboardState.IsKeyDown(Keys.Left)) { player.moveXAxis(-1.5f); } else if (keyboardState.IsKeyDown(Keys.Right)) { player.moveXAxis(1.5f); } Code:
public void limitLocationX(int minimum, int maximum) { if (position.X < minimum) { position.X = minimum; } else if (position.X > maximum) { position.X = maximum; } } Code:
public void limitLocationX(int minimum, int maximum) { position.X = MathHelper.Clamp(position.X, minimum, maximum); } ![]() Code:
player.limitLocationX(13, 224 - 13 - 13); The next thing we're going to tackle is shooting the bullet. It's pretty similar to the player. First, download this https://dl.dropboxusercontent.com/u/...ial/Bullet.png Like before, right click TalkingInvadersContent (Content) on the right side of the project, selected Add, then Existing Item and browse to wherever you downloaded the bullet image. Next, declare a variable of type Texture2D called playerBulletSprite like this: Code:
Texture2D playerBulletSprite; Code:
playerBulletSprite = Content.Load<Texture2D>("Bullet"); Code:
if (keyboardState.IsKeyDown(Keys.Space)) { playerBullet = new Sprite(player.getPosition(), playerBulletSprite); } Code:
playerBullet = new Sprite(new Vector2(0, -1000), playerBulletSprite); Code:
public void setPosition(Vector2 p) { position = p; } Code:
if (keyboardState.IsKeyDown(Keys.Space)) { if (playerBullet.getPosition().Y < -4) // completely off screen? { // position the bullet on top of the player playerBullet.setPosition(new Vector2(player.getPosition().X + 6, // + 6 is the middle of a 13 wide sprite player.getPosition().Y - 4)); // - 4 is on top of the player sprite } } Code:
spriteBatch.Draw(playerBullet.getImage(), playerBullet.getPosition(), Color.White); Code:
public void moveYAxis(float distance) { position.Y = position.Y + distance; } Code:
if (playerBullet.getPosition().Y >= -4) { playerBullet.moveYAxis(-4); } ![]() The new hotness should look like this: ![]() In the next tutorial, I'm going to add the enemies and maybe the shields (I can't think of a easy way to do the shields yet). |
#21
|
||||
|
||||
![]()
Just a heads-up, your inline images in the latest post aren't showing up from here.
|
#22
|
||||
|
||||
![]()
Looks like they're there now. Might've been a dropbox thing
![]() |
#23
|
||||
|
||||
![]()
Just posting to say I'm excited to get back to this, which probably won't be until Sunday, at which point I'll have forgotten everything I've learned and have to start over from the top.
|
#24
|
||||
|
||||
![]()
Aaaand done. I had a brief issue at the very beginning with the draw function because it didn't like that I added the ship and was saying I needed to Begin the SpriteBatch before I could end it, even though I hadn't deleted or touched SpriteBatch.Begin(); I had to move the SpriteBatch.End(); line a couple of times before it eventually took it. Not sure why, I kept it at the end of the code the whole time. Weird! Anyway it works now and I didn't have any trouble after that. Thanks Balrog.
|
#25
|
||||
|
||||
![]()
Yeah, all of the spriteBatch.Draw method calls have to have a spriteBatch.Begin(); before and a spriteBatch.End(); afterwards.
|
#26
|
||||
|
||||
![]()
This looks amazing and I'm definitely going to follow along with it soon. Thanks, Balrog!
|
#27
|
||||
|
||||
![]()
Chiming in late to note that the shields are probably easiest handled as separate sprite chunks to make up each "damage" section of the shield (since you could shoot them from underneath as well). Positioning and collision will be a pain in the ass, but you should be able to reuse the player/enemy collision code to do it.
|
#28
|
||||
|
||||
![]()
Alternately you can have the shields each be a customizable pixel array that keeps track of the "live" parts of the shield, and then a custom collision-detection routine that gets called whenever a shot intersects with the rectangle-box of the shield to see whether it actually hits the currently active shield and removes some active pixels as appropriate. Of course, you'd also need a custom drawing routine, or just write out a new sprite every time one's damaged. All that's quite a bit advanced beyond the current lesson. But it's probably what I'd do for realsies?
|
#29
|
||||
|
||||
![]()
The next thing we're going to tackle is the aliens. First, download these:
https://dl.dropboxusercontent.com/u/...rial/Small.png https://dl.dropboxusercontent.com/u/...ial/Medium.png https://dl.dropboxusercontent.com/u/...rial/Large.png Like before, right click on TalkingInvadersContent (Content) on the right hand side and then Add, then Existing Item, then browse to where you downloaded those. Next we'll make three new Texture2Ds to handle these. Put this towards the top of your Game1.cs file with the other Texture2Ds: Code:
Texture2D smallSprite, mediumSprite, largeSprite; Code:
smallSprite = Content.Load<Texture2D>("Small"); mediumSprite = Content.Load<Texture2D>("Medium"); largeSprite = Content.Load<Texture2D>("Large"); Code:
alien = new Sprite(new Vector2(32,64), smallSprite); Code:
spriteBatch.Draw(alien.getImage(), alien.getPosition(), Color.White); ![]() We can shoot at the alien but nothing happens ![]() Code:
int[] ages; Code:
Sprite[] aliens; into this: Code:
aliens = new Sprite[55]; ![]() Code:
aliens[0] = new Sprite(new Vector2(32,64), smallSprite); // array indexes in C# start at 0! aliens[1] = new Sprite(new Vector2(40,64), smallSprite); aliens[2] = new Sprite(new Vector2(48,64), smallSprite); // etc. Code:
aliens = new Sprite[55]; // this has to be called first, otherwise the code below will crash :( for (int x = 0; x < 11; x++) { aliens[x] = new Sprite(new Vector2(32 + (x * 16), 64), smallSprite); } I did a little mathemagic to get the sprites positioned right, I start at x:32 then move over 16 pixels to the right to set the next position. So we have a for loop that makes a row of 11 enemies and we need 5 of those, so we'll wrap the for loop in another for loop and have it go 5 times like this: Code:
for (int y = 0; y < 5; y++) { for (int x = 0; x < 11; x++) { aliens[x + (y * 11)] = new Sprite(new Vector2(32 + (x * 16), 64), smallSprite); } } Code:
for (int i = 0; i < 55; i++) { spriteBatch.Draw(aliens[i].getImage(), aliens[i].getPosition(), Color.White); } Code:
for (int y = 0; y < 5; y++) { for (int x = 0; x < 11; x++) { aliens[x + (y * 11)] = new Sprite(new Vector2(32 + (x * 16), 64 + (y * 16)), smallSprite); // note the 64 + ( y * 16) } } Unfortunately, the sprites are all the same. So I'm going to add some if/else statements in here to change that to the right ones. Code:
for (int y = 0; y < 5; y++) { for (int x = 0; x < 11; x++) { if (y == 0) // first row { aliens[x + (y * 11)] = new Sprite(new Vector2(32 + (x * 16), 64 + (y * 16)), smallSprite); } else if (y == 1 || y == 2) // second or third row { aliens[x + (y * 11)] = new Sprite(new Vector2(32 + (x * 16), 64 + (y * 16)), mediumSprite); } else // everything else { aliens[x + (y * 11)] = new Sprite(new Vector2(32 + (x * 16), 64 + (y * 16)), largeSprite); } } } At this point, if you hit F5, you should get this: ![]() The next thing we're going to do is make the enemies disappear when we hit them. Probably the simplest way of doing this is to add a new variable to our Sprite class that acts as an on/off switch for rendering. The best fit data type-wise for on/off is a bool which has a value of true or false (like a light switch...no dimmer!). So inside our Sprite class we're going to add a new bool variable called visible and create getVisible(), setVisible(...) functions for it. Add this within the Sprite.cs file: Code:
bool visible; public bool getVisible() { return visible; } public void setVisible(bool v) { visible = v; } // constructor public Sprite(Vector2 p, Texture2D i, bool v) { position = p; image = i; visible = v; } Code:
player = new Sprite(new Vector2(103, 208), shipSprite, true); playerBullet = new Sprite(new Vector2(-1000, -1000), playerBulletSprite, true); aliens = new Sprite[55]; for (int y = 0; y < 5; y++) { for (int x = 0; x < 11; x++) { if (y == 0) // first row { aliens[x + (y * 11)] = new Sprite(new Vector2(32 + (x * 16), 64 + (y * 16)), smallSprite, true); } else if (y == 1 || y == 2) // second or third row { aliens[x + (y * 11)] = new Sprite(new Vector2(32 + (x * 16), 64 + (y * 16)), mediumSprite, true); } else // everything else { aliens[x + (y * 11)] = new Sprite(new Vector2(32 + (x * 16), 64 + (y * 16)), largeSprite, true); } } } Code:
if (aliens[i].getVisible() == true) { spriteBatch.Draw(aliens[i].getImage(), aliens[i].getPosition(), Color.White); } Let's start by making a new function in the Sprite class that returns a Rectangle called hitbox like this: Code:
public Rectangle hitbox() { return new Rectangle((int)position.X, (int)position.Y, image.Width, image.Height); } Now that we have a way to get a Sprite's hitbox, we need to check the bullet's hitbox against the enemy's hitbox. We'll use a for loop like before but this time in the Update method in the Game1.cs file. Code:
for (int i = 0; i < aliens.Length; i++) { if (playerBullet.hitbox().Intersects(aliens[i].hitbox())) { // hide the alien aliens[i].setVisible(false); // move the bullet offscreen playerBullet.setPosition(new Vector2(-1000, -1000)); } } ![]() Code:
for (int i = 0; i < aliens.Length; i++) { if (aliens[i].getVisible() == true && playerBullet.hitbox().Intersects(aliens[i].hitbox())) { // hide the alien aliens[i].setVisible(false); // move the bullet offscreen playerBullet.setPosition(new Vector2(-1000, -1000)); } } The next thing we need to do is move the aliens back and forth around the screen. They either move left or right and move down when 1 enemy hits the side of the screen. Since the aliens move left and right in unison we don't really need a isMovingRight variable in the Sprite class and we can't put it inside the Update method because it will get reset each time so we'll put it at the top of the Game1.cs file with the rest of the variables like this: Code:
bool isMovingRight = true; Code:
for (int i = 0; i < aliens.Length; i++) { if (aliens[i].getVisible() && playerBullet.hitbox().Intersects(aliens[i].hitbox())) { // hide the alien aliens[i].setVisible(false); // move the bullet offscreen playerBullet.setPosition(new Vector2(-1000, -1000)); } if (isMovingRight) { aliens[i].moveXAxis(.05f); } else { aliens[i].moveXAxis(-.05f); } } Code:
bool atLeastOneOffscreen = false; for (int i = 0; i < aliens.Length; i++) { if (aliens[i].getVisible() && playerBullet.hitbox().Intersects(aliens[i].hitbox())) { // hide the alien aliens[i].setVisible(false); // move the bullet offscreen playerBullet.setPosition(new Vector2(-1000, -1000)); } if (isMovingRight) { aliens[i].moveXAxis(.05f); } else { aliens[i].moveXAxis(-.05f); } // check if anyone moved offscreen if (!atLeastOneOffscreen && aliens[i].getVisible() && (aliens[i].getPosition().X < 0 || aliens[i].getPosition().X > 224 - aliens[i].hitbox().Width)) { atLeastOneOffscreen = true; isMovingRight = !isMovingRight; } } if (atLeastOneOffscreen) { for (int i = 0; i < aliens.Length; i++) { aliens[i].moveYAxis(1); // move down aliens[i].limitLocationX(0, 224 - aliens[i].hitbox().Width); // don't let them go off screen } } It turns true into false and false into true. You can also use it with the equal sign like this "5 != 6" which means 5 does not equal 6. So this statement "isMovingRight = !isMovingRight;" switches whatever the value of isMovingRight is to it's opposite. If it's true it becomes false, if it's false it becomes true. If you hit F5, you can test it out. It should look like this after you've taken some aliens out: ![]() Next we're going to add point values to the aliens. I could just add a point value to the Sprite class but then the player would have a point value and bullets would too and that kinda doesn't make sense. So we're going to make a new class that inherits from Sprite called Alien. When a class inherits from another class it receives all the variables and functions from the parent class. If you want you can override those parent functions or just use the ones you get. So right click on TalkingInvaders on the right hand side, then Add, then Class..., then type in Alien.cs in the texbox towards the bottom, then click the Add button in the bottom right. Erase whatever they give you for the Alien.cs class and put this in: Code:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace TalkingInvaders { public class Alien : Sprite { int points; public int getPoints() { return points; } public Alien(Vector2 p, Texture2D i, bool v, int pnts) : base(p, i, v) { points = pnts; } } } Code:
Alien[] aliens; Code:
aliens = new Sprite[55]; Code:
aliens = new Alien[55]; Code:
if (y == 0) // first row { aliens[x + (y * 11)] = new Sprite(new Vector2(32 + (x * 16), 64 + (y * 16)), smallSprite, true); } else if (y == 1 || y == 2) // second or third row { aliens[x + (y * 11)] = new Sprite(new Vector2(32 + (x * 16), 64 + (y * 16)), mediumSprite, true); } else // everything else { aliens[x + (y * 11)] = new Sprite(new Vector2(32 + (x * 16), 64 + (y * 16)), largeSprite, true); } Code:
if (y == 0) // first row { aliens[x + (y * 11)] = new Alien(new Vector2(32 + (x * 16), 64 + (y * 16)), smallSprite, true, 10); // 10 points } else if (y == 1 || y == 2) // second or third row { aliens[x + (y * 11)] = new Alien(new Vector2(32 + (x * 16), 64 + (y * 16)), mediumSprite, true, 20); // 20 points } else // everything else { aliens[x + (y * 11)] = new Alien(new Vector2(32 + (x * 16), 64 + (y * 16)), largeSprite, true, 30); // 30 points } Code:
int player1ScoreVal; Code:
// up the score player1ScoreVal += aliens[i].getPoints(); // could also be written like this // player1ScoreVal = player1ScoreVal + aliens[i].getPoints(); Code:
public void setText(string t) { text = t; } Code:
player1Score.setText(player1ScoreVal.ToString()); Code:
string scoreVal = player1ScoreVal.ToString(); player1Score.setText(scoreVal.PadLeft(4, '0')); // score is 4 digits long Last edited by Balrog; 03-14-2016 at 09:28 PM. |
#30
|
||||
|
||||
![]()
Got to inserting For Loops to display rows of aliens when I got this error in the Draw function:
![]() I only get this error when I type aliens.getImage and aliens.getPosition. I don't get it with alien.getImage or alien.getPosition but instead get 'alien does not exist in the current context'. I noticed I had been using alien as a variable instead of aliens at one point and thought I missed one somewhere in one of the classes but find and replace hasn't found anything. As far as I can tell all instances of 'alien' have been replaced to 'aliens'. Not sure what's going on. |
![]() |
Tags |
diy , programming |
Thread Tools | |
Display Modes | |
|
|