3
\$\begingroup\$

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);
        }
    }
}
\$\endgroup\$
1
  • 2
    \$\begingroup\$ Good question, however I wouldn't get too hung up on the answer - just do whatever works and get on with the rest of your game. I too often over-think things like this, remember that you can always come back and change it later if it causes you problems elsewhere (in fact this is probably the best way to learn as it teaches you why the approach you chose is inferior). For what its worth I'd put the IsSnakeEatingApple method in the Game class as its the only one that "knows" about both the snake and the apple. Think "how would I change this code if I added oranges?" \$\endgroup\$
    – Justin
    Commented Oct 22, 2012 at 16:08

3 Answers 3

3
\$\begingroup\$

According to your game design you need one more class for Map. In the map class

  1. You first define a grid which is the map grid whose walls have a texture.
  2. Create a snake object inside the map class.
  3. Inside the map you create an apple object at random position after every apple is eaten.
  4. Now you have everything snake, walls and apple inside the map class.
  5. All you have to do is detect movement and collisions in update method of map. As per your question where to detect collision, you detect collision inside map using intersects method of rectangle. If free space then valid move, if wall texture or snake texture then snake dead.
\$\endgroup\$
2
\$\begingroup\$

You can have a CollisionManager class to detect every collision, it makes some sense if you think about the real world, we don't actually detect collisions, we just feel then when they happen. You can make an ICollidable interface and your CollisionManager can call an onCollision method, this way the snake doesnt have to know about the walls or the apple, they just have to know what kind of object they collided with (apple, wall, etc) and can react accordingly.

Also, I see no problem with Game knowing about the Snake, it has to and the simplest way is to query the Snake for that. If you keep trying to make a perfect architecture, you will never make a game. Relax and use whatever works for your game.

edit:

Something like this (I don't know C#, but Ill try to make the code understandable):

interface ICollidable
{
    void onCollision(ICollidable other);
}

class Snake implements ICollidable
{
    void onCollision(ICollidable other)
    {
        if(other typeof Apple)
            grow();
        [...]
    }
}

class CollisionManager
{
    list<ICollidable> collidables;
    void addCollidable(ICollidable object){...}
    void update(float delta)
    {
        for(ICollidable c in collidables)
        {
            if(collision(c)) c.onCollision(...);
        }
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ Please if you expand a little with some pseudo-code it would help understand your concept. \$\endgroup\$
    – sprocket12
    Commented Oct 22, 2012 at 14:52
  • \$\begingroup\$ @MuhammadA is that enough? if you have a specific question I can edit further \$\endgroup\$
    – Luke B.
    Commented Oct 22, 2012 at 15:44
1
\$\begingroup\$

Questions about snake games are rather common, you should first check those for general directions:

For your specific question, I think you should use less polling and more pushing. You need to make sure your objects depend on the "game state" (objects push to the game), but not the other way around (game polls from the objects): circular dependencies are evil.

Here's what I would do for your two specific scenarios:

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.

Let your snake affect the score every time it updates. E.g. add a new method such as Game1::SetScore() and call it from Snake::Update().

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

As you figured yourself, and as Chandan Pednekar answered, you need some kind of map/level object, basically a 2D array of tile types for a simple snake. Push your snake "body parts" to the map, add a method to find a random free spot that take those in account, and use this to place new apples.

\$\endgroup\$
2
  • \$\begingroup\$ If I use a 2D array of the same size as a body block/ apple size, and place my snake body bits and apples in there, it makes finding blanks much easier but does this mean I have to lose the snake class, as that is what managed the positions of the body bits of the snake up until now? \$\endgroup\$
    – sprocket12
    Commented Oct 22, 2012 at 15:39
  • \$\begingroup\$ @MuhammadA Not necessarily, you can duplicate the info that's needed for the map (i.e. which tiles the snake occupies), but there are probably some infos that can be kept private to the snake: the textures to use, the current speed... \$\endgroup\$ Commented Oct 23, 2012 at 9:03

Not the answer you're looking for? Browse other questions tagged .