I am making a snake game and learning XNA at the same time. I have 3 classes :
Game.cs, Snake.cs and Apple.cs
My problem is more of a conceptual problem, I want to know which class is really responsible for ...
detecting collision of snake head on apple/itself/wall?
which class should increase the snakes speed, size?
It seems to me that however much I try and put the snake stuff into snake.cs that game.cs has to know a lot about the snake, like :
-- I want to increase the score depending on size of snake, the score variable is inside game.cs, which means now I have to ask the snake its size on every hit of the apple... seems a bit unclean all this highly coupled code.
or
-- I DO NOT want to place the apple under the snake... now the apple suddenly has to know about all the snake parts, my head hurts when I think of that. Maybe there should be some sort of AppleLayer.cs class that should know about the snake...
Whats the best approach in such a simple scenario? Any tips welcome.
Game.cs :
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Design;
namespace Snakez
{
public enum CurrentGameState
{
Playing,
Paused,
NotPlaying
}
public class Game1 : Microsoft.Xna.Framework.Game
{
private GraphicsDeviceManager _graphics;
private SpriteBatch _spriteBatch;
private readonly Color _niceGreenColour = new Color(167, 255, 124);
private KeyboardState _oldKeyboardState;
private SpriteFont _scoreFont;
private SoundEffect _biteSound, _crashSound;
private Vector2 _scoreLocation = new Vector2(10, 10);
private Apple _apple;
private Snake _snake;
private int _score = 0;
private int _speed = 1;
public Game1()
{
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
/// <summary>
/// Allows the game to perform any initialization it needs to before starting to run.
/// This is where it can query for any required services and load any non-graphic
/// related content. Calling base.Initialize will enumerate through any components
/// and initialize them as well.
/// </summary>
protected override void Initialize()
{
base.Initialize();
}
/// <summary>
/// LoadContent will be called once per game and is the place to load
/// all of your content.
/// </summary>
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
_scoreFont = Content.Load<SpriteFont>("Score");
_apple = new Apple(800, 480, Content.Load<Texture2D>("Apple"));
_snake = new Snake(Content.Load<Texture2D>("BodyBlock"));
_biteSound = Content.Load<SoundEffect>("Bite");
_crashSound = Content.Load<SoundEffect>("Crash");
}
/// <summary>
/// UnloadContent will be called once per game and is the place to unload
/// all content.
/// </summary>
protected override void UnloadContent()
{
Content.Unload();
}
/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update(GameTime gameTime)
{
KeyboardState newKeyboardState = Keyboard.GetState();
if (newKeyboardState.IsKeyDown(Keys.Escape))
{
this.Exit(); // Allows the game to exit
}
else if (newKeyboardState.IsKeyDown(Keys.Up) && !_oldKeyboardState.IsKeyDown(Keys.Up))
{
_snake.SetDirection(Direction.Up);
}
else if (newKeyboardState.IsKeyDown(Keys.Down) && !_oldKeyboardState.IsKeyDown(Keys.Down))
{
_snake.SetDirection(Direction.Down);
}
else if (newKeyboardState.IsKeyDown(Keys.Left) && !_oldKeyboardState.IsKeyDown(Keys.Left))
{
_snake.SetDirection(Direction.Left);
}
else if (newKeyboardState.IsKeyDown(Keys.Right) && !_oldKeyboardState.IsKeyDown(Keys.Right))
{
_snake.SetDirection(Direction.Right);
}
_oldKeyboardState = newKeyboardState;
_snake.Update();
if (_snake.IsEating(_apple))
{
_biteSound.Play();
_score += 10;
_apple.Place();
}
base.Update(gameTime);
}
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(_niceGreenColour);
float frameRate = 1 / (float)gameTime.ElapsedGameTime.TotalSeconds;
_spriteBatch.Begin();
_spriteBatch.DrawString(_scoreFont, "Score : " + _score, _scoreLocation, Color.Red);
_apple.Draw(_spriteBatch);
_snake.Draw(_spriteBatch);
_spriteBatch.End();
base.Draw(gameTime);
}
}
}
Snake.cs :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
namespace Snakez
{
public enum Direction
{
Up,
Down,
Left,
Right
}
public class Snake
{
private List<Rectangle> _parts;
private readonly Texture2D _bodyBlock;
private readonly int _startX = 160;
private readonly int _startY = 120;
private int _moveDelay = 100;
private DateTime _lastUpdatedAt;
private Direction _direction;
private Rectangle _lastTail;
public Snake(Texture2D bodyBlock)
{
_bodyBlock = bodyBlock;
_parts = new List<Rectangle>();
_parts.Add(new Rectangle(_startX, _startY, _bodyBlock.Width, _bodyBlock.Height));
_parts.Add(new Rectangle(_startX + bodyBlock.Width, _startY, _bodyBlock.Width, _bodyBlock.Height));
_parts.Add(new Rectangle(_startX + (bodyBlock.Width) * 2, _startY, _bodyBlock.Width, _bodyBlock.Height));
_parts.Add(new Rectangle(_startX + (bodyBlock.Width) * 3, _startY, _bodyBlock.Width, _bodyBlock.Height));
_direction = Direction.Right;
_lastUpdatedAt = DateTime.Now;
}
public void Draw(SpriteBatch spriteBatch)
{
foreach (var p in _parts)
{
spriteBatch.Draw(_bodyBlock, new Vector2(p.X, p.Y), Color.White);
}
}
public void Update()
{
if (DateTime.Now.Subtract(_lastUpdatedAt).TotalMilliseconds > _moveDelay)
{
//DateTime.Now.Ticks
_lastTail = _parts.First();
_parts.Remove(_lastTail);
/* add new head in right direction */
var lastHead = _parts.Last();
var newHead = new Rectangle(0, 0, _bodyBlock.Width, _bodyBlock.Height);
switch (_direction)
{
case Direction.Up:
newHead.X = lastHead.X;
newHead.Y = lastHead.Y - _bodyBlock.Width;
break;
case Direction.Down:
newHead.X = lastHead.X;
newHead.Y = lastHead.Y + _bodyBlock.Width;
break;
case Direction.Left:
newHead.X = lastHead.X - _bodyBlock.Width;
newHead.Y = lastHead.Y;
break;
case Direction.Right:
newHead.X = lastHead.X + _bodyBlock.Width;
newHead.Y = lastHead.Y;
break;
}
_parts.Add(newHead);
_lastUpdatedAt = DateTime.Now;
}
}
public void SetDirection(Direction newDirection)
{
if (_direction == Direction.Up && newDirection == Direction.Down)
{
return;
}
else if (_direction == Direction.Down && newDirection == Direction.Up)
{
return;
}
else if (_direction == Direction.Left && newDirection == Direction.Right)
{
return;
}
else if (_direction == Direction.Right && newDirection == Direction.Left)
{
return;
}
_direction = newDirection;
}
public bool IsEating(Apple apple)
{
if (_parts.Last().Intersects(apple.Location))
{
GrowBiggerAndFaster();
return true;
}
return false;
}
private void GrowBiggerAndFaster()
{
_parts.Insert(0, _lastTail);
_moveDelay -= (_moveDelay / 100)*2;
}
}
}
Apple.cs :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
namespace Snakez
{
public class Apple
{
private readonly int _maxWidth, _maxHeight;
private readonly Texture2D _texture;
private readonly Random random = new Random();
public Rectangle Location { get; private set; }
public Apple(int screenWidth, int screenHeight, Texture2D texture)
{
_maxWidth = (screenWidth + 1) - texture.Width;
_maxHeight = (screenHeight + 1) - texture.Height;
_texture = texture;
Place();
}
public void Place()
{
Location = GetRandomLocation(_maxWidth, _maxHeight);
}
private Rectangle GetRandomLocation(int maxWidth, int maxHeight)
{
// x and y -- multiple of 20
int x = random.Next(1, maxWidth);
var leftOver = x % 20;
x = x - leftOver;
int y = random.Next(1, maxHeight);
leftOver = y % 20;
y = y - leftOver;
return new Rectangle(x, y, _texture.Width, _texture.Height);
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(_texture, Location, Color.White);
}
}
}