Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
1
branch
0
tags
Code
-
Use Git or checkout with SVN using the web URL.
-
Open with GitHub Desktop
-
Download ZIP
Latest commit
Files
Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
C# Tutorial — Create a Classic Snakes Game in Visual Studio UPDATED
The classic snake game project is now updated and added with some extra features to boost your game play and power up your C# programming skills using the Windows Form Framework in Visual Studio.
From the last project the following were added to the new and updated one
- You can go through the walls from up/down/left/right and appear on the other side with reversed speed
- You can Take keep the score and save a highscore from any game plays session
- You can take snap shot of the game and save it as JPG file with the Score and Highscore as a caption in the image
- Start and Restart game with a button on run time
Link to the Website —
C# Tutorial – How to make a Classic Snakes Game with Windows form and Visual Studio [Updated]
Full Video Tutorial on the official Channel —
About
Create the fun classic snakes game using C# in Windows Form. We will be using little elements of OOP programming to make this project, the code, project files and video tutorial is all presented with this project here
Topics
Resources
Readme
License
Apache-2.0 license
Stars
2
stars
Watchers
1
watching
Forks
5
forks
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing.Imaging; // add this for the JPG compressor
namespace Classic_Snakes_Game_Tutorial___MOO_ICT
{
public partial class Form1 : Form
{
private List<Circle> Snake = new List<Circle>();
private Circle food = new Circle();
int maxWidth;
int maxHeight;
int score;
int highScore;
Random rand = new Random();
bool goLeft, goRight, goDown, goUp;
public Form1()
{
InitializeComponent();
new Settings();
}
private void KeyIsDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Left && Settings.directions != «right»)
{
goLeft = true;
}
if (e.KeyCode == Keys.Right && Settings.directions != «left»)
{
goRight = true;
}
if (e.KeyCode == Keys.Up && Settings.directions != «down»)
{
goUp = true;
}
if (e.KeyCode == Keys.Down && Settings.directions != «up»)
{
goDown = true;
}
}
private void KeyIsUp(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Left)
{
goLeft = false;
}
if (e.KeyCode == Keys.Right)
{
goRight = false;
}
if (e.KeyCode == Keys.Up)
{
goUp = false;
}
if (e.KeyCode == Keys.Down)
{
goDown = false;
}
}
private void StartGame(object sender, EventArgs e)
{
RestartGame();
}
private void TakeSnapShot(object sender, EventArgs e)
{
Label caption = new Label();
caption.Text = «I scored: « + score + » and my Highscore is « + highScore + » on the Snake Game from MOO ICT»;
caption.Font = new Font(«Ariel», 12, FontStyle.Bold);
caption.ForeColor = Color.Purple;
caption.AutoSize = false;
caption.Width = picCanvas.Width;
caption.Height = 30;
caption.TextAlign = ContentAlignment.MiddleCenter;
picCanvas.Controls.Add(caption);
SaveFileDialog dialog = new SaveFileDialog();
dialog.FileName = «Snake Game SnapShot MOO ICT»;
dialog.DefaultExt = «jpg»;
dialog.Filter = «JPG Image File | *.jpg»;
dialog.ValidateNames = true;
if (dialog.ShowDialog() == DialogResult.OK)
{
int width = Convert.ToInt32(picCanvas.Width);
int height = Convert.ToInt32(picCanvas.Height);
Bitmap bmp = new Bitmap(width, height);
picCanvas.DrawToBitmap(bmp, new Rectangle(0,0, width, height));
bmp.Save(dialog.FileName, ImageFormat.Jpeg);
picCanvas.Controls.Remove(caption);
}
}
private void GameTimerEvent(object sender, EventArgs e)
{
// setting the directions
if (goLeft)
{
Settings.directions = «left»;
}
if (goRight)
{
Settings.directions = «right»;
}
if (goDown)
{
Settings.directions = «down»;
}
if (goUp)
{
Settings.directions = «up»;
}
// end of directions
for (int i = Snake.Count — 1; i >= 0; i—)
{
if (i == 0)
{
switch (Settings.directions)
{
case «left»:
Snake[i].X—;
break;
case «right»:
Snake[i].X++;
break;
case «down»:
Snake[i].Y++;
break;
case «up»:
Snake[i].Y—;
break;
}
if (Snake[i].X < 0)
{
Snake[i].X = maxWidth;
}
if (Snake[i].X > maxWidth)
{
Snake[i].X = 0;
}
if (Snake[i].Y < 0)
{
Snake[i].Y = maxHeight;
}
if (Snake[i].Y > maxHeight)
{
Snake[i].Y = 0;
}
if (Snake[i].X == food.X && Snake[i].Y == food.Y)
{
EatFood();
}
for (int j = 1; j < Snake.Count; j++)
{
if (Snake[i].X == Snake[j].X && Snake[i].Y == Snake[j].Y)
{
GameOver();
}
}
}
else
{
Snake[i].X = Snake[i — 1].X;
Snake[i].Y = Snake[i — 1].Y;
}
}
picCanvas.Invalidate();
}
private void UpdatePictureBoxGraphics(object sender, PaintEventArgs e)
{
Graphics canvas = e.Graphics;
Brush snakeColour;
for (int i = 0; i < Snake.Count; i++)
{
if (i == 0)
{
snakeColour = Brushes.Black;
}
else
{
snakeColour = Brushes.DarkGreen;
}
canvas.FillEllipse(snakeColour, new Rectangle
(
Snake[i].X * Settings.Width,
Snake[i].Y * Settings.Height,
Settings.Width, Settings.Height
));
}
canvas.FillEllipse(Brushes.DarkRed, new Rectangle
(
food.X * Settings.Width,
food.Y * Settings.Height,
Settings.Width, Settings.Height
));
}
private void RestartGame()
{
maxWidth = picCanvas.Width / Settings.Width — 1;
maxHeight = picCanvas.Height / Settings.Height — 1;
Snake.Clear();
startButton.Enabled = false;
snapButton.Enabled = false;
score = 0;
txtScore.Text = «Score: « + score;
Circle head = new Circle { X = 10, Y = 5 };
Snake.Add(head); // adding the head part of the snake to the list
for (int i = 0; i < 100; i++)
{
Circle body = new Circle();
Snake.Add(body);
}
food = new Circle { X = rand.Next(2, maxWidth), Y = rand.Next(2, maxHeight)};
gameTimer.Start();
}
private void EatFood()
{
score += 1;
txtScore.Text = «Score: « + score;
Circle body = new Circle
{
X = Snake[Snake.Count — 1].X,
Y = Snake[Snake.Count — 1].Y
};
Snake.Add(body);
food = new Circle { X = rand.Next(2, maxWidth), Y = rand.Next(2, maxHeight) };
}
private void GameOver()
{
gameTimer.Stop();
startButton.Enabled = true;
snapButton.Enabled = true;
if (score > highScore)
{
highScore = score;
txtHighScore.Text = «High Score: « + Environment.NewLine + highScore;
txtHighScore.ForeColor = Color.Maroon;
txtHighScore.TextAlign = ContentAlignment.MiddleCenter;
}
}
}
}
Предисловие
Привет Хабр! Меня зовут Евгений «Nage», и я начал заниматься программированием около года назад, в свободное от работы время. Просмотрев множество различных туториалов по программированию задаешься вопросом «а что же делать дальше?», ведь в основном все рассказывают про самые основы и дальше как правило не заходят. Вот после продолжительного времени за просмотром разных роликов про одно и тоже я решил что стоит двигаться дальше, и браться за первый проект. И так, сейчас мы разберем как можно написать игру «Змейка» в консоли со своими начальными знаниями.
Глава 1. Итак, с чего начнем?
Для начала нам ничего лишнего не понадобится, только блокнот (или ваш любимый редактор), и компилятор C#, он присутствует по умолчанию в Windows, находится он в С:WindowsMicrosoft.NETFrameworkv4.0.30319csc.exe. Можно использовать компилятор последней версии который поставляется с visual studio, он находится Microsoft Visual Studio2017CommunityMSBuild15.0BinRoslyncsc.exe.
Создадим файл для быстрой компиляции нашего кода, сохранил файл с расширением .bat со следующим содержимым:
@echo off
:Start
set /p name= Enter program name:
echo.
С:WindowsMicrosoft.NETFrameworkv4.0.30319csc.exe "%name%.cs"
echo.
goto Start
«@echo off» отключает отображение команд в консоли. С помощью команды goto получаем бесконечный цикл. Задаем переменную name, а с модификатором /p в переменную записывается значение введенное пользователем в консоль. «echo.» просто оставляет пустую строчку в консоли. Далее вызываем компилятор и передаем ему файл нашего кода, который он скомпилирует.
Таким способом мы можем скомпилировать только один файл, поэтому мы будем писать все классы в одном документе (я не разобрался еще как компилировать несколько файлов в один .exe через консоль, да и это не тема нашей статьи, может кто нибудь расскажет в комментариях).
Для тех кто сразу хочет увидеть весь код.
Скрытый текст
using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
namespace SnakeGame
{
class Game
{
static readonly int x = 80;
static readonly int y = 26;
static Walls walls;
static Snake snake;
static FoodFactory foodFactory;
static Timer time;
static void Main()
{
Console.SetWindowSize(x + 1, y + 1);
Console.SetBufferSize(x + 1, y + 1);
Console.CursorVisible = false;
walls = new Walls(x, y, '#');
snake = new Snake(x / 2, y / 2, 3);
foodFactory = new FoodFactory(x, y, '@');
foodFactory.CreateFood();
time = new Timer(Loop, null, 0, 200);
while (true)
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo key = Console.ReadKey();
snake.Rotation(key.Key);
}
}
}// Main()
static void Loop(object obj)
{
if (walls.IsHit(snake.GetHead()) || snake.IsHit(snake.GetHead()))
{
time.Change(0, Timeout.Infinite);
}
else if (snake.Eat(foodFactory.food))
{
foodFactory.CreateFood();
}
else
{
snake.Move();
}
}// Loop()
}// class Game
struct Point
{
public int x { get; set; }
public int y { get; set; }
public char ch { get; set; }
public static implicit operator Point((int, int, char) value) =>
new Point {x = value.Item1, y = value.Item2, ch = value.Item3};
public static bool operator ==(Point a, Point b) =>
(a.x == b.x && a.y == b.y) ? true : false;
public static bool operator !=(Point a, Point b) =>
(a.x != b.x || a.y != b.y) ? true : false;
public void Draw()
{
DrawPoint(ch);
}
public void Clear()
{
DrawPoint(' ');
}
private void DrawPoint(char _ch)
{
Console.SetCursorPosition(x, y);
Console.Write(_ch);
}
}
class Walls
{
private char ch;
private List<Point> wall = new List<Point>();
public Walls(int x, int y, char ch)
{
this.ch = ch;
DrawHorizontal(x, 0);
DrawHorizontal(x, y);
DrawVertical(0, y);
DrawVertical(x, y);
}
private void DrawHorizontal(int x, int y)
{
for (int i = 0; i < x; i++)
{
Point p = (i, y, ch);
p.Draw();
wall.Add(p);
}
}
private void DrawVertical(int x, int y)
{
for (int i = 0; i < y; i++)
{
Point p = (x, i, ch);
p.Draw();
wall.Add(p);
}
}
public bool IsHit(Point p)
{
foreach (var w in wall)
{
if (p == w)
{
return true;
}
}
return false;
}
}// class Walls
enum Direction
{
LEFT,
RIGHT,
UP,
DOWN
}
class Snake
{
private List<Point> snake;
private Direction direction;
private int step = 1;
private Point tail;
private Point head;
bool rotate = true;
public Snake(int x, int y, int length)
{
direction = Direction.RIGHT;
snake = new List<Point>();
for (int i = x - length; i < x; i++)
{
Point p = (i, y, '*');
snake.Add(p);
p.Draw();
}
}
public Point GetHead() => snake.Last();
public void Move()
{
head = GetNextPoint();
snake.Add(head);
tail = snake.First();
snake.Remove(tail);
tail.Clear();
head.Draw();
rotate = true;
}
public bool Eat(Point p)
{
head = GetNextPoint();
if (head == p)
{
snake.Add(head);
head.Draw();
return true;
}
return false;
}
public Point GetNextPoint ()
{
Point p = GetHead ();
switch (direction)
{
case Direction.LEFT:
p.x -= step;
break;
case Direction.RIGHT:
p.x += step;
break;
case Direction.UP:
p.y -= step;
break;
case Direction.DOWN:
p.y += step;
break;
}
return p;
}
public void Rotation (ConsoleKey key)
{
if (rotate)
{
switch (direction)
{
case Direction.LEFT:
case Direction.RIGHT:
if (key == ConsoleKey.DownArrow)
direction = Direction.DOWN;
else if (key == ConsoleKey.UpArrow)
direction = Direction.UP;
break;
case Direction.UP:
case Direction.DOWN:
if (key == ConsoleKey.LeftArrow)
direction = Direction.LEFT;
else if (key == ConsoleKey.RightArrow)
direction = Direction.RIGHT;
break;
}
rotate = false;
}
}
public bool IsHit(Point p)
{
for (int i = snake.Count - 2; i > 0; i--)
{
if (snake[i] == p)
{
return true;
}
}
return false;
}
}//class Snake
class FoodFactory
{
int x;
int y;
char ch;
public Point food { get; private set; }
Random random = new Random();
public FoodFactory(int x, int y, char ch)
{
this.x = x;
this.y = y;
this.ch = ch;
}
public void CreateFood()
{
food = (random.Next(2, x - 2), random.Next(2, y - 2), ch);
food.Draw();
}
}
}
Глава 2. Первые шаги
Подготовим поле нашей игры, начиная с точки входа в нашу программу. Задаем переменные X и Y, размер и буфер окна консоли, и скроем отображение курсора.
using System;
using System.Collections.Generic;
using System.Linq;
class Game{
static readonly int x = 80;
static readonly int y = 26;
static void Main(){
Console.SetWindowSize(x + 1, y + 1);
Console.SetBufferSize(x + 1, y + 1);
Console.CursorVisible = false;
}// Main()
}// class Game
Для вывода на экран нашей «графики» создадим свой тип данных — точка. Он будет содержать координаты и символ, который будет выводится на экран. Также сделаем методы для вывода на экран точки и ее «стирания».
struct Point{
public int x { get; set; }
public int y { get; set; }
public char ch { get; set; }
public static implicit operator Point((int, int, char) value) =>
new Point {x = value.Item1, y = value.Item2, ch = value.Item3};
public void Draw(){
DrawPoint(ch);
}
public void Clear(){
DrawPoint(' ');
}
private void DrawPoint(char _ch){
Console.SetCursorPosition(x, y);
Console.Write(_ch);
}
}
Это интересно!
Оператор => называется лямбда-оператор, он используется в качестве определения анонимных лямбда выражений, и в качестве тела, состоящего из одного выражения, синтаксический сахар, заменяющий оператор return. Приведенный выше метод переопределения оператора (про его назначение чуть ниже) можно переписать так:public static bool operator ==(Point a, Point b){ if (a.x == b.x && a.y == b.y){ return true; } else{ return false; } }
Создадим класс стен, границы игрового поля. Напишем 2 метода на создание вертикальных и горизонтальных линий, и в конструкторе вызываем отрисовку всех 4х сторон заданным символом. Список всех точек в стенке нам пригодится позже.
class Walls{
private char ch;
private List<Point> wall = new List<Point>();
public Walls(int x, int y, char ch){
this.ch = ch;
DrawHorizontal(x, 0);
DrawHorizontal(x, y);
DrawVertical(0, y);
DrawVertical(x, y);
}
private void DrawHorizontal(int x, int y){
for (int i = 0; i < x; i++){
Point p = (i, y, ch);
p.Draw();
wall.Add(p);
}
}
private void DrawVertical(int x, int y) {
for (int i = 0; i < y; i++) {
Point p = (x, i, ch);
p.Draw();
wall.Add(p);
}
}
}// class Walls
Это интересно!
Как вы могли заметить для инициализации типа данных Point используется форма Point p = (x, y, ch); как и у встроенных типов, это становится возможным при переопределении оператора implicit, в котором описывается как задаются переменные.
Важно!
Конструкция (int, int, char) называется кортежем, и работает только с .net 4.7+, по этому если у вас не установлен visual studio, то в вашем распоряжении только компилятор v4.0.30319 и нужно использовать стандартную инициализацию через оператор new.
Вернемся к классу Game и объявим поле walls, а в методе Main инициализируем ее.
class Game{
static Walls walls;
static void Main(){
walls = new Walls(x, y, '#');
...
Все! Можно скомпилировать код и посмотреть, что наше поле построилось, и самая легкая часть позади.
Глава 3. А что сегодня на завтрак?
Добавим генерацию еды на нашем поле, для этого создадим класс FoodFactory, который и будет заниматься созданием еды внутри границ.
class FoodFactory
{
int x;
int y;
char ch;
public Point food { get; private set; }
Random random = new Random();
public FoodFactory(int x, int y, char ch)
{
this.x = x;
this.y = y;
this.ch = ch;
}
public void CreateFood()
{
food = (random.Next(2, x - 2), random.Next(2, y - 2), ch);
food.Draw();
}
}
Добавляем инициализацию фабрики и создадим еду на поле
class Game{
static FoodFactory foodFactory;
static void Main(){
foodFactory = new FoodFactory(x, y, '@');
foodFactory.CreateFood();
...
Кушать подано!
Глава 4. Время главного героя
Перейдем к созданию самой змеи, и для начала определим перечисление направления движения змейки.
enum Direction{
LEFT,
RIGHT,
UP,
DOWN
}
Теперь можем создать класс змейки, где опишем как она будет ползать, поворачивать. Определим список точек змеи, наше перечисление, шаг на сколько будет перемещаться за ход, и ссылки на хвостовую и головную точки, и конструктор, в котором рисуем змею в заданных координатах и заданной длинны при старте игры.
class Snake{
private List<Point> snake;
private Direction direction;
private int step = 1;
private Point tail;
private Point head;
bool rotate = true;
public Snake(int x, int y, int length){
direction = Direction.RIGHT;
snake = new List<Point>();
for (int i = x - length; i < x; i++) {
Point p = (i, y, '*');
snake.Add(p);
p.Draw();
}
}
//Методы движения и поворота в зависимости он направления движения змейки.
public Point GetHead() => snake.Last();
public void Move(){
head = GetNextPoint();
snake.Add(head);
tail = snake.First();
snake.Remove(tail);
tail.Clear();
head.Draw();
rotate = true;
}
public Point GetNextPoint() {
Point p = GetHead();
switch (direction) {
case Direction.LEFT:
p.x -= step;
break;
case Direction.RIGHT:
p.x += step;
break;
case Direction.UP:
p.y -= step;
break;
case Direction.DOWN:
p.y += step;
break;
}
return p;
}
public void Rotation(ConsoleKey key) {
if (rotate) {
switch (direction) {
case Direction.LEFT:
case Direction.RIGHT:
if (key == ConsoleKey.DownArrow)
direction = Direction.DOWN;
else if (key == ConsoleKey.UpArrow)
direction = Direction.UP;
break;
case Direction.UP:
case Direction.DOWN:
if (key == ConsoleKey.LeftArrow)
direction = Direction.LEFT;
else if (key == ConsoleKey.RightArrow)
direction = Direction.RIGHT;
break;
}
rotate = false;
}
}
}//class Snake
В методе поворота, что бы избежать возможности повернуть сразу на 180 градусов, просто указываем, что в каждом направлении мы можем повернуть только в 2 стороны. А проблему поворота на 180 градусов двумя нажатиями — поставив «переключатель», отключаем возможность поворачивать после первого нажатия, и включаем после очередного хода.
Осталось вывести ее на экран.
class Game{
static Snake snake;
static void Main(){
snake = new Snake(x / 2, y / 2, 3);
...
Готово! теперь у нас есть все что нужно, поле огороженное стенами, рандомно появляющаяся еда, и змейка. Пришла пора заставить все это взаимодействовать друг с другом.
Глава 5. Л-логика
Заставим нашу змейку двигаться, напишем бесконечный цикл для считывания клавиш нажатых на клавиатуре, и передаем клавишу в метод поворота змеи
class Game {
static void Main () {
while (true) {
if (Console.KeyAvailable) {
ConsoleKeyInfo key = Console.ReadKey ();
snake.Rotation(key.Key);
}
...
для движения змеи воспользуемся классом .net который будет запускать метод Loop через определенные промежутки времени.
using System.Threading;
class Game {
static Timer time;
static void Main () {
time = new Timer (Loop, null, 0, 200);
...
Теперь, перед тем как написать метод движения змейки, надо реализовать взаимодействие головы с едой, стенками и хвостом змеи. Для этого надо написать метод, позволяющий сравнивать две точки на совпадение координат. Переопределим оператор равенства и не равенства, их обязательно нужно переопределять в паре.
struct Point {
public static bool operator == (Point a, Point b) =>
(a.x == b.x && a.y == b.y) ? true : false;
public static bool operator != (Point a, Point b) =>
(a.x != b.x || a.y != b.y) ? true : false;
...
Теперь можно написать метод, который будет проверять совпадает ли интересующая нас точка с какой нибудь из массива стен.
class Walls {
public bool IsHit (Point p) {
foreach (var w in wall) {
if (p == w) {
return true;
}
}
return false;
}
...
И похожий метод проверяющий не совпадает ли точка с хвостом.
class Snake {
public bool IsHit (Point p) {
for (int i = snake.Count - 2; i > 0; i--) {
if (snake[i] == p) {
return true;
}
}
return false;
}
...
И методом проверки съела ли еду наша змейка, и сразу делаем ее длиннее.
class Snake {
public bool Eat (Point p) {
head = GetNextPoint ();
if (head == p) {
snake.Add (head);
head.Draw ();
return true;
}
return false;
}
...
теперь можно написать метод движения, со всеми нужными проверками.
class Snake {
static void Loop (object obj) {
if (walls.IsHit (snake.GetHead ()) || snake.IsHit (snake.GetHead ())) {
time.Change (0, Timeout.Infinite);
} else if (snake.Eat (foodFactory.food)) {
foodFactory.CreateFood ();
} else {
snake.Move ();
}
}
...
Вот и все! Наша змейка в консоли закончена и можно поиграть.
Заключение
Мы посмотрели как можно реализовать первую простенькую игру с небольшим использованием ООП, научились перегружать операторы, посмотрели на кортежи и лямбда оператор, надеюсь это было полезно!
Это была пилотная статья, и если вам понравилось, я напишу про реализацию змейки на Unity.
Всем удачи!
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace Snake { public partial class Form1 : Form { Bitmap view; Graphics g; // графическая поверхность List<Rectangle> snake = new List<Rectangle>(); // змейка public enum Course {UP, DOWN, LEFT, RIGHT }; public int lengthSnake; // длина змейки public bool existsFood; // существование еды на поле Rectangle food = new Rectangle(); // поле с едой List<Rectangle> empty_filed = new List<Rectangle>(); // список незанятых ячеек public Random rnd = new Random(); public Course course; // направление движения public int addX, addY; // смещение координат public Form1() { InitializeComponent(); course = Course.LEFT; // начальное направление // добавляем в змейку три начальных сегмента snake.Add(new Rectangle(200, 0, 20, 20)); snake.Add(new Rectangle(220, 0, 20, 20)); snake.Add(new Rectangle(240, 0, 20, 20)); timer1.Interval = 200; // устанавливаем таймер lengthSnake = 3; // начальная длина змейки existsFood = false; // еды нет g = this.CreateGraphics();// создаем графичскую поверхность DrawSnake(); // рисуем змейку DrawFood(); // рисуем еду timer1.Enabled = true; // запускаем таймер } // Form1 private void Form1_TickTimer(object sender, EventArgs e) { //Refresh(); // очищаем экран g.Clear(this.BackColor); timer1.Enabled = false; // останавливаем таймер // в зависимости от выбранного направления // наращиваем координаты головы (snake[0]) if (course == Course.UP) { addX = 0; addY = -20; } if (course == Course.DOWN) { addX = 0; addY = 20; } if (course == Course.LEFT) { addX = -20; addY = 0; } if (course == Course.RIGHT) { addX = 20; addY = 0; } Rectangle prev_segment; Rectangle next_segment; prev_segment = snake[0]; // запоминаем значение старой головы // чтобы присвоить его след. сегменту // по циклу присваиваем значение предыдущего сегмента следующему for (int i = 0; i < snake.Count - 1; i++) { if (i == 0) { snake[i] = new Rectangle( snake[i].X + addX, snake[i].Y + addY, 20, 20); } if (!(snake[i + 1].IsEmpty)) { next_segment = snake[i + 1]; snake[i + 1] = prev_segment; prev_segment = next_segment; } } // если голова "съела" еду if (snake[0] == food) { snake.Add(food); // добавляем сегмент с коор-ами еды // этот сегмент пройдет через всю змейку // и "прицепиться" в конце lengthSnake++; // увеличиваем длину змейки existsFood = false; // еды нет // увеличиваем скорость через каждые 7 сегментов змейки if ((lengthSnake % 7 == 0) && (timer1.Interval > 50) ) { timer1.Interval -= 30; } } // Проверка на проигрыш if (// выход за границу игрового поля (snake[0].X < 0 || snake[0].X > 240 || snake[0].Y < 0 || snake[0].Y > 240) || // проверка на самосъедение EatMySelf()) { MessageBox.Show("Игра законченаn Длина змейки равна: " + lengthSnake.ToString()); this.Close(); return; } DrawSnake(); // рисуем змейку DrawFood(); // рисуем еду pictureBox1.Image = (Image)view; timer1.Enabled = true; // запускаем таймер } // Form1_TickTimer // метод меняет направление, в зависимости от нажатой стрелки private void Form1_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Up && course != Course.DOWN) { course = Course.UP; } if (e.KeyCode == Keys.Down && course != Course.UP) { course = Course.DOWN; } if (e.KeyCode == Keys.Left && course != Course.RIGHT) { course = Course.LEFT; } if (e.KeyCode == Keys.Right && course != Course.LEFT) { course = Course.RIGHT; } } // Form1_KeyDown private void DrawSnake() { for (int i = 0; i < snake.Count; i++) { if (i == 0) // рисуем голову g.FillRectangle(Brushes.Green, snake[i]); else // рисуем оставшиеся сегменты g.FillRectangle(Brushes.Black, snake[i]); g.DrawRectangle(Pens.Green, snake[i]); } } //DrawSnake private void DrawFood() { // если еда съедена - русуем новую if (!existsFood) { for (int i = 0; i < 13; i++) { for (int j = 0; j < 13; j++) { Rectangle temp = new Rectangle(i * 20, j * 20, 20, 20); // заполняем список свободных сегментов игрового поля if (snake.IndexOf(temp) == -1) { empty_filed.Add(temp); } } } // находим случайным образом свободную ячейку food = empty_filed[rnd.Next(0, empty_filed.Count - 1)]; empty_filed.Clear(); // очищаем список свободных сегментов игрового поля g.FillRectangle(Brushes.Red, food); // рисуем еду existsFood = true; //еда есть } else { // иначе рисуем старую еду g.FillRectangle(Brushes.Red, food); } } // проверка на самосъедение private bool EatMySelf() { int count = 0; // количество сегментов равных голове foreach (Rectangle t in snake) { if (t == snake[0]) count++; } // еда совпадает с координатами головы (это не считаем за самосъедение) if (count > 1 && food != snake[0]) return true; else return false; } // EatMySelf private void Form1_Shown(object sender, EventArgs e) { view = new Bitmap(270, 270); // инициализируем второй буфер g = Graphics.FromImage((Image)view); // и графику для него } } }
In this page, we are sharing the source code of a «simple snake game» developed on C# programming language as a windows form application. Here are the most relevant keywords about this game: Graphics, Timer, Double Buffering, First Game, Game Loop.
You can download the source code of the snake game from here. Please download the project, and build it with Visual Studio to run the application.
Note that, the application is written in C# programming language, using old style Windows Form Applications as UI components. Try to use WPF (windows presentation foundation) for better development and maintainability experiences.
How to improve the game?
1. Level: Update timer1.Interval to be 20 smaller when the size of the snake reaches multiplicatives of 10.
2. Walls: Add 3-5 blue points that represent walls. When snake hit them, it is gameover.
3. Score: Eating a food should have an award. Actually, reaching the food faster should be rewarded. When a food appears, you can start countnig from 20 to 1 in each step and add the value to the score.
4. Bonus food: Sometimes you may create other colors of foods that have bonus snake size and points.
5. Scores: Save the best scores and create a Top 10 list.
added 7 years 10 months ago
- Remove From My Forums
-
Question
-
Hi
I need help the make a classic snake game in windows form application. The most guides for the snake game are in console so they don’t really help. If you could post the basic code (or all of the code) i would be very thankfull.
-
Moved by
Tuesday, March 17, 2015 5:03 PM
Winforms related
-
Moved by
Answers
-
This is a very broad question. For that reason, I will provide a broad answer. If you need code specific help, please post code specific questions.
This is my way of thinking and it may not be the best solution:
1- You need to create a virtual grid on your form.
Let say you have a 800 x 800 form and you will divide this into 20 x 20 grids.
Each grid will be 40 px width and height.
Create a grid class to hold x, y values.
2- For your snake you need to create a snake class.
Snake class will hold the length of the snake.
3 — Game class:
This will be the main container class that holds snake, timer (if available), bates for the snake, game logic.
4- Game logic Class:
in your game logic class, define how your snake behaves for example when a user clicks up button while snakes direction is left.
5- Form elements:
Each grid will hold a picture box and your picture box will be filled with different elements such as:
— plain
— bate
— snake
6 — Collusion detection:
This will be a part of game logic but it will be better if you have separate collusion detection class.
-
Proposed as answer by
Carl Cai
Wednesday, March 18, 2015 4:28 AM -
Marked as answer by
Carl Cai
Wednesday, March 25, 2015 5:51 AM
-
Proposed as answer by
-
Hi,
Please try to follow
Val10’s ideas and write it by yourself.You also can put some keywords like «C# snake game » in the search engine.
It will shows many resluts about it. You could choose to refer someone to get some hints.
This project seems meet your requirements. Please take a look
#Snake Game in C#
http://www.c-sharpcorner.com/uploadfile/prink_mob/snake-game-in-C-Sharp/
If all of these are not what you want, please submit a sample request to
http://code.msdn.microsoft.com/windowsapps/site/requests.
If you encountered any issue when implementing it, you could open new thread
for each specific issue.Best regards,
Kristin
We are trying to better understand customer views on social support experience, so your participation in this interview project would be greatly appreciated if you have time. Thanks for helping make community forums a great place.
Click
HERE to participate the survey.-
Edited by
Kristin Xie
Tuesday, March 17, 2015 8:17 AM -
Proposed as answer by
Carl Cai
Wednesday, March 18, 2015 4:28 AM -
Marked as answer by
Carl Cai
Wednesday, March 25, 2015 5:51 AM
-
Edited by
-
-
Proposed as answer by
Carl Cai
Wednesday, March 18, 2015 4:28 AM -
Marked as answer by
Carl Cai
Wednesday, March 25, 2015 5:51 AM
-
Proposed as answer by
-
-
Proposed as answer by
Carl Cai
Wednesday, March 18, 2015 4:28 AM -
Marked as answer by
Carl Cai
Wednesday, March 25, 2015 5:51 AM
-
Proposed as answer by
Как уменьшить размер исполняемого файла C#?
Как человеку, выросшему во времена дискет и 56 Кбит модемов, мне всегда нравились небольшие программы. Я мог поместить много небольших программ на дискету, которую носил с собой. Если программа не помещалась на моем гибком диске, я начинал думать, почему: много графики? Музыка? Программа сложная или просто раздулась?
В наши дни дисковое пространство стало настолько дешевым (а огромные флешки настолько вездесущими), что люди отказались от оптимизации размера.
Единственная область, где размер еще имеет значение — это передача: при передаче программы по проводу мегабайты приравниваются к секундам. Быстрое соединение на 100 Мбит может пропускать только 12 мегабайт в секунду в лучшем случае. Когда на другом конце провода находится человек, ожидающий завершения загрузки, разница между пятью и одной секундами может оказать существенное влияние на восприятие. Человек может зависеть от времени передачи либо напрямую: он загружает программу по сети, либо косвенно — бессерверная служба развертывается для ответа на веб-запрос.
Люди обычно воспринимают что-то быстрее 0,1 секунды как мгновенное. 3 секунды — примерно предел непрерывности потока пользователя, и вам было бы трудно удержать пользователя после 10 секунд.
Хотя меньший размер больше не является существенным, он всё равно лучше.
Эта статья вышла как эксперимент, чтобы выяснить, насколько маленьким может быть полезный автономный исполняемый файл C#. Могут ли приложения C# достичь размеров, при которых пользователи посчитают время загрузки мгновенным? Возможно ли использовать C# в тех местах, где язык не используется сейчас?
Что такое автономность?
Автономное приложение — это приложение, включающее в себя все необходимое для запуска на операционной системе. Компилятор C# принадлежит к группе компиляторов, нацеленных на виртуальную машину (Java и Kotlin — другие заметные члены группы). Выходные данные компилятора C# — это исполняемый файл, требующий выполнения некоторой виртуальной машины. Нельзя просто установить чистую операционную систему и ожидать, что на ней можно запускать программы, созданные компилятором C#.
По крайней мере Windows — тот случай, когда можно полагаться на машинную установку .NET Framework для запуска выходных данных компилятора C#. В настоящее время есть много инструментов без этого ограничения: IoT Nano Server, ARM64. Платформа .NET Framework также не поддерживает последние усовершенствования языка C#.
Для автономности приложение на C# должно включать среду выполнения и все используемые библиотеки классов. Это много, чтобы вписаться в планируемые 8 Кб!
Игра меньше 8 Кб
Мы создадим клон змейки:
Не интересна игровая механика? Не стесняйтесь переходить к интересным частям, где мы сжимаем игру с 65 мегабайт до 8 килобайт за 9 шагов, прокрутите до графика.
Игра будет работать в текстовом режиме, и мы используем поле рисования символов, чтобы нарисовать змею. Я уверен, что Vulcan или DirectX намного веселее, но мы справимся и с System.Console
.
Игра без выделения памяти
Мы собираемся создать игру без выделения памяти, и под этим я не имею в виду “не выделять память в цикле игры”, распространённое среди разработчиков игр C#. Я имею в виду запрет ключевого слова new
со ссылочными типами во всей кодовой базе. Причины станут очевидны на заключительном шаге сжатия игры.
При таком ограничении можно задаться вопросом, есть ли вообще смысл в использовании C#: без new
мы не будем использовать сборщик мусора, не сможем выбрасывать исключения и т.д. То есть язык С будет работать так же хорошо.
Одна из причин использования C# — “потому что это возможно”. Другая причина — тестируемость и общий доступ к коду. Хотя игра не выделяет память для ссылочных типов, это не означает, что ее части не могут повторно использоваться в другом проекте, не имеющем таких ограничений. Например, части игры могут быть включены в проекта xUnit, чтобы покрыть приложение юнит-тестами. Если кто-то выбирает C для сборки игры, всё ограничено возможностями C, даже если код используется повторно. Но поскольку C# обеспечивает хорошее сочетание конструкций высокого и низкого уровня абстракций, мы можем использовать высокий уровень по умолчанию, а низкий уровень при необходимости. Для достижения размера развертывания в 8 Кб потребуется низкоуровневая часть.
Структура игры
Начнем со структуры буфера кадров. Буфер кадров — это компонент, содержащий пиксели (или в данном случае — символы), отображаемые на экране:
unsafe struct FrameBuffer
{
public const int Width = 40;
public const int Height = 20;
public const int Area = Width * Height;
fixed char _chars[Area];
public void SetPixel(int x, int y, char character)
{
_chars[y * Width + x] = character;
}
public void Clear()
{
for (int i = 0; i < Area; i++)
_chars[i] = ' ';
}
public readonly void Render()
{
Console.SetCursorPosition(0, 0);
const ConsoleColor snakeColor = ConsoleColor.Green;
Console.ForegroundColor = snakeColor;
for (int i = 1; i <= Area; i++)
{
char c = _chars[i - 1];
if (c == '*' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
{
Console.ForegroundColor = c == '*' ? ConsoleColor.Red : ConsoleColor.White;
Console.Write(c);
Console.ForegroundColor = snakeColor;
}
else
Console.Write(c);
if (i % Width == 0)
{
Console.SetCursorPosition(0, i / Width - 1);
}
}
}
}
Мы предоставляем методы установки отдельных пикселей, очистки и визуализации содержимого буфера кадров System.Console
. Шаг рендеринга особых случаев несколько символов, так что мы получаем красочный вывод без необходимости отслеживать цвет каждого пикселя буфера кадров.
Одна интересная вещь — поле fixed char _chars[Area]
: это синтаксис C# для объявления фиксированного массива . Фиксированный массив — это массив, отдельные элементы которого являются частью структуры. Вы можете думать об этом как о ярлыке для набора полей char _char_0, _char_1, _char_2, _char_3,... _char_Area
, к которым можно получить доступ как в массиве. Размер этого массива должен быть постоянной времени компиляции, чтобы размер всей структуры был фиксированным.
Мы не можем переборщить с размером фиксированного массива, потому что как часть структуры массив должен жить в стеке, а стеки, как правило, ограничены небольшим количеством байтов (обычно 1 Мб на поток) и 40*20*2 width*height* sizeof(char)
— допустимое число.
Следующее, что нам нужно — генератор случайных чисел. Поставляемый с .NET генератор является ссылочным типом по уважительным причинам, а мы запретили себе new
. Но простую struct
сделаем:
struct Random
{
private uint _val;
public Random(uint seed)
{
_val = seed;
}
public uint Next() => _val = (1103515245 * _val + 12345) % 2147483648;
}
Этот генератор не слишком хорош, но нам не нужно ничего сложного. Теперь пишем обёртку для логики игры:
struct Snake
{
public const int MaxLength = 30;
private int _length;
/*
_body - упакованное целое число для координат и символ для тела змеи. Только примитивные типы могут быть использованы с fixed, следовательно, это int. */
private unsafe fixed int _body[MaxLength];
private Direction _direction;
private Direction _oldDirection;
public Direction Course
{
set
{
if (_oldDirection != _direction)
_oldDirection = _direction;
if (_direction - value != 2 && value - _direction != 2)
_direction = value;
}
}
public unsafe Snake(byte x, byte y, Direction direction)
{
_body[0] = new Part(x, y, DirectionToChar(direction, direction)).Pack();
_direction = direction;
_oldDirection = direction;
_length = 1;
}
public unsafe bool Update()
{
Part oldHead = Part.Unpack(_body[0]);
Part newHead = new Part(
(byte)(_direction switch
{
Direction.Left => oldHead.X == 0 ? FrameBuffer.Width - 1 : oldHead.X - 1,
Direction.Right => (oldHead.X + 1) % FrameBuffer.Width,
_ => oldHead.X,
}),
(byte)(_direction switch
{
Direction.Up => oldHead.Y == 0 ? FrameBuffer.Height - 1 : oldHead.Y - 1,
Direction.Down => (oldHead.Y + 1) % FrameBuffer.Height,
_ => oldHead.Y,
}),
DirectionToChar(_direction, _direction)
);
oldHead = new Part(oldHead.X, oldHead.Y, DirectionToChar(_oldDirection, _direction));
bool result = true;
for (int i = 0; i < _length - 1; i++)
{
Part current = Part.Unpack(_body[i]);
if (current.X == newHead.X && current.Y == newHead.Y)
result = false;
}
_body[0] = oldHead.Pack();
for (int i = _length - 2; i >= 0; i--)
{
_body[i + 1] = _body[i];
}
_body[0] = newHead.Pack();
_oldDirection = _direction;
return result;
}
public unsafe readonly void Draw(ref FrameBuffer fb)
{
for (int i = 0; i < _length; i++)
{
Part p = Part.Unpack(_body[i]);
fb.SetPixel(p.X, p.Y, p.Character);
}
}
public bool Extend()
{
if (_length < MaxLength)
{
_length += 1;
return true;
}
return false;
}
public unsafe readonly bool HitTest(int x, int y)
{
for (int i = 0; i < _length; i++)
{
Part current = Part.Unpack(_body[i]);
if (current.X == x && current.Y == y)
return true;
}
return false;
}
private static char DirectionToChar(Direction oldDirection, Direction newDirection)
{
const string DirectionChangeToChar = "│┌?┐┘─┐??└│┘└?┌─";
return DirectionChangeToChar[(int)oldDirection * 4 + (int)newDirection];
}
// Вспомогательная структура для работы с координатами тела.
readonly struct Part
{
public readonly byte X, Y;
public readonly char Character;
public Part(byte x, byte y, char c)
{
X = x;
Y = y;
Character = c;
}
public int Pack() => X << 24 | Y << 16 | Character;
public static Part Unpack(int packed) => new Part((byte)(packed >> 24), (byte)(packed >> 16), (char)packed);
}
public enum Direction
{
Up, Right, Down, Left
}
}
Состояния, которые должна отслеживать змейка:
- Координаты каждого пикселя тела.
- Длина змейки.
- Текущее направление движения.
- Прошлое направление для случая, когда нужно нарисовать символ изгиба вместо прямой линии.
Структура предоставляет метод расширения змеи на один элемент (возвращает false, если змея уже находится на полной длине), метод HitTest
для теста столкновений пикселя тела, отрисовки змейки во FrameBuffer
и метод обновления положения змеи как ответ на игровой тик (возвращает false
, если змея съела себя). Существует также свойство, чтобы установить направление змеи.
Мы используем тот же трюк с фиксированным массивом, что и в буфере кадров, чтобы не использовать new
. Это означает, что максимальная длина змеи должна быть постоянной времени компиляции. Последнее, что нам нужно — игровой цикл:
struct Game
{
enum Result
{
Win, Loss
}
private Random _random;
private Game(uint randomSeed)
{
_random = new Random(randomSeed);
}
private Result Run(ref FrameBuffer fb)
{
Snake s = new Snake(
(byte)(_random.Next() % FrameBuffer.Width),
(byte)(_random.Next() % FrameBuffer.Height),
(Snake.Direction)(_random.Next() % 4));
MakeFood(s, out byte foodX, out byte foodY);
long gameTime = Environment.TickCount64;
while (true)
{
fb.Clear();
if (!s.Update())
{
s.Draw(ref fb);
return Result.Loss;
}
s.Draw(ref fb);
if (Console.KeyAvailable)
{
ConsoleKeyInfo ki = Console.ReadKey(intercept: true);
switch (ki.Key)
{
case ConsoleKey.UpArrow:
s.Course = Snake.Direction.Up; break;
case ConsoleKey.DownArrow:
s.Course = Snake.Direction.Down; break;
case ConsoleKey.LeftArrow:
s.Course = Snake.Direction.Left; break;
case ConsoleKey.RightArrow:
s.Course = Snake.Direction.Right; break;
}
}
if (s.HitTest(foodX, foodY))
{
if (s.Extend())
MakeFood(s, out foodX, out foodY);
else
return Result.Win;
}
fb.SetPixel(foodX, foodY, '*');
fb.Render();
gameTime += 100;
long delay = gameTime - Environment.TickCount64;
if (delay >= 0)
Thread.Sleep((int)delay);
else
gameTime = Environment.TickCount64;
}
}
void MakeFood(in Snake snake, out byte foodX, out byte foodY)
{
do
{
foodX = (byte)(_random.Next() % FrameBuffer.Width);
foodY = (byte)(_random.Next() % FrameBuffer.Height);
}
while (snake.HitTest(foodX, foodY));
}
static void Main()
{
Console.SetWindowSize(FrameBuffer.Width, FrameBuffer.Height);
Console.SetBufferSize(FrameBuffer.Width, FrameBuffer.Height);
Console.Title = "See Sharp Snake";
Console.CursorVisible = false;
FrameBuffer fb = new FrameBuffer();
while (true)
{
Game g = new Game((uint)Environment.TickCount64);
Result result = g.Run(ref fb);
string message = result == Result.Win ? "You win" : "You lose";
int position = (FrameBuffer.Width - message.Length) / 2;
for (int i = 0; i < message.Length; i++)
{
fb.SetPixel(position + i, FrameBuffer.Height / 2, message[i]);
}
fb.Render();
Console.ReadKey(intercept: true);
}
}
}
Мы используем генератор случайных чисел для генерации случайного положения и направления змеи. Мы случайным образом размещаем еду на игровой поверхности так, чтобы она не перекрывала змею, и запускаем цикл игры. Внутри игрового цикла мы просим змею обновить свое положение и проверить, съела ли она сама себя. Затем рисуем змею, проверяем клавиатуру на ввод, тестируем змею с едой и отображаем всё на консоль. Давайте посмотрим, где мы находимся с точки зрения размера.
Размер змейки в .NET Core 3.0 по умолчанию
Я поместил игру в репозиторий GitHub, чтобы вы могли следить за ней. Файл проекта собирает игру в различных конфигурациях в зависимости от переданного при публикации свойства Mode
. Чтобы создать конфигурацию по умолчанию с помощью CoreCLR, выполните:
dotnet publish -r win-x64 -c Release
Это приведет к созданию одного EXE-файла, имеющего колоссальный размер в 65 МБ. Этот файл включает в себя игру, рантайм .NET и библиотеки базовых классов, являющиеся стандартной частью .NET. Вы можете сказать: “Лучше, чем Electron”, но давайте посмотрим, сможем ли мы уменьшить размер.
Il Linker
IL Linker — это инструмент, поставляемый с .NET Core 3.0. Он удаляет неиспользуемые сборки. Чтобы включить его в проект, передайте свойство PublishTrimmed
при публикации:
dotnet publish -r win-x64 -c Release /p:PublishTrimmed=true
С этой настройкой игра сжимается до 25 МБ. Хорошее сокращение, но оно далеко от нашей цели.
IL Linker имеет более агрессивные настройки, не выставляемые публично, и они могут работать дальше, но в конце концов, мы ограничимся размером самой среды выполнения CoreCLR coreclr.dll
в 5,3 Мбайт. Возможно, мы зашли в тупик на пути к игре на 8 Кб?
Моно
Mono — еще одна среда выполнения .NET, для многих — синоним Xamarin. Чтобы создать исполняемый файл C#, мы используем mkbundle
, поставляемый с Mono:
mkbundle SeeSharpSnake.dll --simple -o SeeSharpSnake.exe
Команда создаст исполняемый файл размером 12,3 МБ, зависящий от mono-2.0-sgen.dll
, а она сама по себе весит 5,9 МБ, так что мы получили 18,2 MB в общей сложности. При попытке запустить его я получил сообщение Error mapping file: mono_file_map_error failed
, но это ожидаемый баг. За исключением этой ошибки, все работает и наш результат — 18,2 МБ.
В отличие от CoreCLR, Mono также зависит от распространяемой библиотеки среды выполнения Visual C++, недоступной в установке Windows по умолчанию: чтобы сохранить автономность приложения, нам нужно зашить эту библиотеку в приложение. Это увеличивает объем ещё на один мегабайт или около того.
Мы, вероятно, сможем сделать приложение меньше, добавив Il Linker, но тогда столкнемся с той же проблемой, что и с CoreCLR — размером среды выполнения mono-2.0-sgen.dll
, он составляет 5,9 МБ. Плюс размер библиотек времени выполнения C++ поверх него. Это предел оптимизаций уровня IL.
Среда выполнения
Чтобы получить размер 8 Кб, нам нужно изъять из приложения среду выполнения или её часть. Единственная среда выполнения .NET, где это возможно — CoreRT. Хотя обычно CoreRT называют “средой выполнения”, она ближе к тому, чтобы быть “библиотекой среды выполнения”. Это не виртуальная машина, как CoreCLR или Mono. Среда выполнения CoreRT — это просто набор функций, поддерживающих заранее созданный компилятором Core RT машинный код.
CoreRT поставляется с библиотеками, делающими CoreRT похожим на любую другую среду выполнения .NET: есть библиотека, добавляющая GC, библиотека поддержки рефлексии, библиотека, добавляющая JIT, библиотека, добавляющая интерпретатор и т.д. Но все они необязательны, включаяGC.
Давайте посмотрим, где мы находимся теперь с конфигурацией CoreRT по умолчанию:
dotnet publish -r win-x64 -c Release /p:Mode=CoreRT
4,7 МБ. Файл пока самый маленький, но этого недостаточно.
Умеренная экономия размера в CoreRT
Компилятор CoreRT предлагает огромное количество настроек, влияющих на генерацию кода. По умолчанию компилятор пытается максимизировать скорость сгенерированного кода и совместимость с другими средами выполнения .NET за счет размера сгенерированного исполняемого файла.
Компилятор имеет встроенный компоновщик, удаляющий неиспользуемый код. Параметр “CoreRT-Moderate”, определяемый в проекте Snake, ослабляет одно из ограничений на удаление неиспользуемого кода, что позволяет удалить ещё больше. Мы также просим компилятор обменять скорость программы на дополнительные байты. Большинство программ .NET работает просто отлично в этом режиме.
dotnet publish -r win-x64 -c Release /p:Mode=CoreRT-Moderate
Сейчас мы на уровне 4,3 МБ.
Включение сильной экономии в CoreRT
Я сгруппировал еще несколько вариантов компиляции в режим “сильной экономии”. Режим удаляет поддержку возможностей, важных для многих приложений, но не для нашей змейки. Мы удаляем:
- Данные трассировки стека для деталей реализации фреймворка.
- Сообщения об исключениях в рамках фреймворка.
- Поддержку неанглийских языков.
- Инструментарий
EventSource
.
dotnet publish -r win-x64 -c Release /p:Mode=CoreRT-High
Мы достигли 3,0 МБ, это 5% от начального размера, но у CoreRT есть еще один трюк.
Отключение рефлексии
Существенная часть библиотек времени выполнения CoreRT посвящена реализации рефлексии .NET. Поскольку CoreRT — это заранее скомпилированная реализация .NET на основе библиотеки времени выполнения, она не нуждается в большинстве структур данных, необходимых типичной среде выполнения на основе виртуальной машины. Эти данные включают такие вещи, как имена типов, методы, подписи, базовые типы и т.д. CoreRT внедряет эти данные, потому что они нужны программам, использующим рефлексию .NET, но не потому, что это необходимо для работы среды выполнения. Я называю эти данные “налогом на рефлексию”, потому что это то, что нужно для выполнения.
CoreRT поддерживает режим без рефлексии, позволяющий избежать оверхеда. Вы можете чувствовать, что много кода .NET не работает без рефлексии, и вы правы, но удивительное количество вещей всё-таки работает: Gui.cs
, System.IO.Pipelines
или даже базовое приложение WinForms. Змейка точно заработает, так что включаем этот режим:
dotnet publish -r win-x64 -c Release /p:Mode=CoreRT-ReflectionFree
Сейчас мы на уровне 1,2 МБ. Оверхед на рефлексию довольно значителен.
Пачкаем руки
Мы достигли предела возможностей .NET SDK, и теперь нам нужно запачкать руки. То, что мы собираемся сделать сейчас, начинает быть смешным, и я бы не ожидал, что кто-то еще это сделает. Мы будем полагаться на детали реализации компилятора CoreRT и среды выполнения.
Как мы уже видели ранее, CoreRT — это набор библиотек времени выполнения в сочетании с опережающим компилятором. Что делать, если мы заменим библиотеки времени выполнения с минимальным переопределением? Мы решили не использовать сборщик мусора, и это делает работу намного более выполнимой. Начнём с простого:
namespace System.Threading
{
static class Thread
{
[DllImport("api-ms-win-core-synch-l1-2-0")]
public static extern void Sleep(int delayMs);
}
}
namespace System
{
static class Environment
{
[DllImport("api-ms-win-core-sysinfo-l1-1-0")]
private static extern long GetTickCount64();
public static long TickCount64 => GetTickCount64();
}
}
Мы просто переопределили Thread.Sleep
иEnvironment.TickCount64
(для Windows), избегая всех зависимостей от существующей библиотеки времени выполнения. Делаем то же самое для подмножества System.Console
, используемого игрой:
namespace System
{
static class Console
{
private enum BOOL : int
{
FALSE = 0,
TRUE = 1,
}
[DllImport("api-ms-win-core-processenvironment-l1-1-0")]
private static unsafe extern IntPtr GetStdHandle(int c);
private readonly static IntPtr s_outputHandle = GetStdHandle(-11);
private readonly static IntPtr s_inputHandle = GetStdHandle(-10);
[DllImport("api-ms-win-core-console-l2-1-0.dll", EntryPoint = "SetConsoleTitleW")]
private static unsafe extern BOOL SetConsoleTitle(char* c);
public static unsafe string Title
{
set
{
fixed (char* c = value)
SetConsoleTitle(c);
}
}
[StructLayout(LayoutKind.Sequential)]
struct CONSOLE_CURSOR_INFO
{
public uint Size;
public BOOL Visible;
}
[DllImport("api-ms-win-core-console-l2-1-0")]
private static unsafe extern BOOL SetConsoleCursorInfo(IntPtr handle, CONSOLE_CURSOR_INFO* cursorInfo);
public static unsafe bool CursorVisible
{
set
{
CONSOLE_CURSOR_INFO cursorInfo = new CONSOLE_CURSOR_INFO
{
Size = 1,
Visible = value ? BOOL.TRUE : BOOL.FALSE
};
SetConsoleCursorInfo(s_outputHandle, &cursorInfo);
}
}
[DllImport("api-ms-win-core-console-l2-1-0")]
private static unsafe extern BOOL SetConsoleTextAttribute(IntPtr handle, ushort attribute);
public static ConsoleColor ForegroundColor
{
set
{
SetConsoleTextAttribute(s_outputHandle, (ushort)value);
}
}
[StructLayout(LayoutKind.Sequential)]
private struct KEY_EVENT_RECORD
{
public BOOL KeyDown;
public short RepeatCount;
public short VirtualKeyCode;
public short VirtualScanCode;
public short UChar;
public int ControlKeyState;
}
[StructLayout(LayoutKind.Sequential)]
private struct INPUT_RECORD
{
public short EventType;
public KEY_EVENT_RECORD KeyEvent;
}
[DllImport("api-ms-win-core-console-l1-2-0", EntryPoint = "PeekConsoleInputW", CharSet = CharSet.Unicode)]
private static unsafe extern BOOL PeekConsoleInput(IntPtr hConsoleInput, INPUT_RECORD* lpBuffer, uint nLength, uint* lpNumberOfEventsRead);
public static unsafe bool KeyAvailable
{
get
{
uint nRead;
INPUT_RECORD buffer;
while (true)
{
PeekConsoleInput(s_inputHandle, &buffer, 1, &nRead);
if (nRead == 0)
return false;
if (buffer.EventType == 1 && buffer.KeyEvent.KeyDown != BOOL.FALSE)
return true;
ReadConsoleInput(s_inputHandle, &buffer, 1, &nRead);
}
}
}
[DllImport("api-ms-win-core-console-l1-2-0", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)]
private static unsafe extern BOOL ReadConsoleInput(IntPtr hConsoleInput, INPUT_RECORD* lpBuffer, uint nLength, uint* lpNumberOfEventsRead);
public static unsafe ConsoleKeyInfo ReadKey(bool intercept)
{
uint nRead;
INPUT_RECORD buffer;
do
{
ReadConsoleInput(s_inputHandle, &buffer, 1, &nRead);
}
while (buffer.EventType != 1 || buffer.KeyEvent.KeyDown == BOOL.FALSE);
return new ConsoleKeyInfo((char)buffer.KeyEvent.UChar, (ConsoleKey)buffer.KeyEvent.VirtualKeyCode, false, false, false);
}
struct SMALL_RECT
{
public short Left, Top, Right, Bottom;
}
[DllImport("api-ms-win-core-console-l2-1-0")]
private static unsafe extern BOOL SetConsoleWindowInfo(IntPtr handle, BOOL absolute, SMALL_RECT* consoleWindow);
public static unsafe void SetWindowSize(int x, int y)
{
SMALL_RECT rect = new SMALL_RECT
{
Left = 0,
Top = 0,
Right = (short)(x - 1),
Bottom = (short)(y - 1),
};
SetConsoleWindowInfo(s_outputHandle, BOOL.TRUE, &rect);
}
[StructLayout(LayoutKind.Sequential)]
struct COORD
{
public short X, Y;
}
[DllImport("api-ms-win-core-console-l2-1-0")]
private static unsafe extern BOOL SetConsoleScreenBufferSize(IntPtr handle, COORD size);
public static void SetBufferSize(int x, int y)
{
SetConsoleScreenBufferSize(s_outputHandle, new COORD { X = (short)x, Y = (short)y });
}
[DllImport("api-ms-win-core-console-l2-1-0")]
private static unsafe extern BOOL SetConsoleCursorPosition(IntPtr handle, COORD position);
public static void SetCursorPosition(int x, int y)
{
SetConsoleCursorPosition(s_outputHandle, new COORD { X = (short)x, Y = (short)y });
}
[DllImport("api-ms-win-core-console-l1-2-0", EntryPoint = "WriteConsoleW")]
private static unsafe extern BOOL WriteConsole(IntPtr handle, void* buffer, int numChars, int* charsWritten, void* reserved);
public static unsafe void Write(char c)
{
int dummy;
WriteConsole(s_outputHandle, &c, 1, &dummy, null);
}
}
}
Пересоберём игру с заменой фреймворка:
dotnet publish-r win-x64-C Release / p: Mode=CoreRT-ReflectionFree /p: IncludePal=true
Неудивительно, что это не слишком эффективно. Заменяемые API уже относительно легки, переписывание только добавляет пару килобайт, о которых не стоит упоминать. Но это важная ступенька к последнему шагу нашего путешествия.
Замена библиотек среды выполнения
Оставшиеся 1,2 МБ кода и данных в игре — это поддержка вещей, которые мы не видим, но они есть, они готовы, если нам понадобятся. Есть сборщик мусора, поддержка обработки исключений, код для форматирования и печати трассировок стека на консоль, когда происходит необработанное исключение, и многие другие вещи под капотом.
Компилятор может обнаружить, что ничего этого не требуется, и избежать их генерации, но то, что мы пытаемся сделать, настолько странно, что неплохо добавить функции компилятора для его поддержки. Способ избежать этого — просто предоставить альтернативную библиотеку времени выполнения. Начнем с переопределения минимальной версии базовых типов:
namespace System
{
public class Object
{
// Слой объекта - контракт с компилятором.
public IntPtr m_pEEType;
}
public struct Void { }
/* Типам ниже для работы не нужны подя. */
public struct Boolean { }
public struct Char { }
public struct SByte { }
public struct Byte { }
public struct Int16 { }
public struct UInt16 { }
public struct Int32 { }
public struct UInt32 { }
public struct Int64 { }
public struct UInt64 { }
public struct IntPtr { }
public struct UIntPtr { }
public struct Single { }
public struct Double { }
public abstract class ValueType { }
public abstract class Enum : ValueType { }
public struct Nullable<T> where T : struct { }
public sealed class String
{
// Слой строки - контракт с компилятором.
public readonly int Length;
public char _firstChar;
public unsafe char this[int index]
{
[System.Runtime.CompilerServices.Intrinsic]
get
{
return Internal.Runtime.CompilerServices.Unsafe.Add(ref _firstChar, index);
}
}
}
public abstract class Array { }
public abstract class Delegate { }
public abstract class MulticastDelegate : Delegate { }
public struct RuntimeTypeHandle { }
public struct RuntimeMethodHandle { }
public struct RuntimeFieldHandle { }
public class Attribute { }
}
namespace System.Runtime.CompilerServices
{
internal sealed class IntrinsicAttribute : Attribute { }
public class RuntimeHelpers
{
public static unsafe int OffsetToStringData => sizeof(IntPtr) + sizeof(int);
}
}
namespace System.Runtime.InteropServices
{
public enum CharSet
{
None = 1,
Ansi = 2,
Unicode = 3,
Auto = 4,
}
public sealed class DllImportAttribute : Attribute
{
public string EntryPoint;
public CharSet CharSet;
public DllImportAttribute(string dllName) { }
}
public enum LayoutKind
{
Sequential = 0,
Explicit = 2,
Auto = 3,
}
public sealed class StructLayoutAttribute : Attribute
{
public StructLayoutAttribute(LayoutKind layoutKind) { }
}
}
namespace Internal.Runtime.CompilerServices
{
public static unsafe partial class Unsafe
{
// Тело метода генерирует компилятор.
// Делает то же, что Unsafe.Add.
[System.Runtime.CompilerServices.Intrinsic]
public static extern ref T Add<T>(ref T source, int elementOffset);
}
}
Теперь откажемся от файла проекта и dotnet CLI, запустим отдельные инструменты напрямую. Мы начинаем с запуска компилятора C# (CSC). Я рекомендую запускать эти команды из “x64 Native Tools Command Prompt for VS 2019” — он находится в меню Пуск, если у вас установлена Visual Studio.
/noconfig
,/nostdlib
, и /runtimemetadataversion
— волшебные параметры, необходимыми для компиляции чего-то, определяющего System.Object
. Я выбрал расширение .ilexe
, потому что .exe
мы используем для готового продукта.
csc.exe /debug /O /noconfig /nostdlib /runtimemetadataversion:v4.0.30319 MiniBCL.cs GameFrameBuffer.cs GameRandom.cs GameGame.cs GameSnake.cs PalThread.Windows.cs PalEnvironment.Windows.cs PalConsole.Windows.cs /out:zerosnake.ilexe /langversion:latest /unsafe
Это позволит успешно скомпилировать версию байт-кода IL игры с компилятором C#. Нам все еще нужна какая-то среда выполнения, чтобы выполнить приложение. Попробуем передать это в CoreRT для создания нативного кода из IL. Если вы выполнили описанные выше шаги, вы найдете ilc.exe, компилятор CoreRT, в вашем кэше пакетов NuGet где-то в %USERPROFILE%.nugetpackagesruntime.win-x64.microsoft.dotnet.ilcompiler1.0.0-alpha-27402–01Tools
.
ilc.exe zerosnake.ilexe -o zerosnake.obj --systemmodule zerosnake --Os -g
Произойдет сбой Expected type ‘Internal.Runtime.CompilerHelpers.StartupCodeHelpers’ not found in module ‘zerosnake’
. Оказывается, помимо очевидного минимума, ожидаемого разработчиком управляемого кода, есть также минимум, в котором компилятор CoreRT нуждается для компиляции ввода. Добавим необходимое:
namespace Internal.Runtime.CompilerHelpers
{
/* Искомый компилятором класс, имеющий помощников для инициализации процесса. Компилятор может изящно обращаться с отсутствующими помощниками, но сам отсутствующий класс остается необработанным. Давайте добавим пустой класс. */
class StartupCodeHelpers
{
}
}
namespace System
{
/* Специальный тип, используемый компилятором для реализации дженериков, например, IEnumerable<T> массивов. Наши массивы не будут реализовывать дженерики. */
class Array<T> : Array { }
}
namespace System.Runtime.InteropServices
{
/* Пользовательский атрибут, помечающий класс как имеющий специальные встроенные функции "вызова". У компилятора специальная логика обработки с этим атрибутом. */
internal class McgIntrinsicsAttribute : Attribute { }
}
namespace System.Runtime.CompilerServices
{
/* Ответственный за выполнение статических конструкторов класс. Код для запуска статических конструкторов и их выполнения единожды. */
[System.Runtime.InteropServices.McgIntrinsics]
internal static class ClassConstructorRunner
{
private static unsafe IntPtr CheckStaticClassConstructionReturnNonGCStaticBase(ref StaticClassConstructionContext context, IntPtr nonGcStaticBase)
{
CheckStaticClassConstruction(ref context);
return nonGcStaticBase;
}
private static unsafe void CheckStaticClassConstruction(ref StaticClassConstructionContext context)
{
/* Очень упрощенный исполнитель конструктора класса. В реальном мире ему необходимо будет иметь возможность работать с потенциально несколькими потоками, участвующими в гонке инициализации класса, и возможность справиться с потенциальными блокировками между конструкторами. */
// Если класс инициализирован, мы закончили.
if (context.initialized == 1)
return;
// Помечает класс как инициализированный.
context.initialized = 1;
// Выполняет конструктор класса.
Call<int>(context.cctorMethodAddress);
}
// Специальная функция, вызывающая метод, указывающий на pfn.
// Компилятор генерирует код. Мы можем просто сделать пометку extern.
// Это станет не нужно, когда язык будет поддерживать указатель на функцию. Планируется в C# 9.
[System.Runtime.CompilerServices.Intrinsic]
private static extern T Call<T>(System.IntPtr pfn);
}
/* Эта структура данных - контракт с компилятором. Она содержит адрес статического конструктора и флаг, определяющий, был ли конструктор выполнен. */
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct StaticClassConstructionContext
{
// Указатель на код для метода конструктора статического класса. Инициализируется binder/runtime.
public IntPtr cctorMethodAddress;
/* Состояние инициализации класса. Этот параметр инициализируется значением 0. Каждый раз, когда управляемый код проверяет состояние конструктора, среда выполнения вызывает CheckStaticClassConstruction с этим контекстом. Структура без инициализации == 1. Эта проверка нужна, чтобы разрешить хранить больше, чем бинарное состояние для каждого конструктора. */
public int initialized;
}
}
Перестроим IL с добавленным кодом и повторно запустим ILC.
csc.exe /debug /O /noconfig /nostdlib /runtimemetadataversion:v4.0.30319 MiniRuntime.cs MiniBCL.cs GameFrameBuffer.cs GameRandom.cs GameGame.cs GameSnake.cs PalThread.Windows.cs PalEnvironment.Windows.cs PalConsole.Windows.cs /out:zerosnake.ilexe /langversion:latest /unsafeilc.exe zerosnake.ilexe -o zerosnake.obj --systemmodule zerosnake --Os -g
Теперь у нас есть zerosnake.obj
— стандартный объектный файл, который ничем не отличается от объектных файлов, создаваемых другими нативными компиляторами, такими как C или C++. Последний шаг — связать его. Воспользуемся link.exe
, он должен быть в “x64 Native Tools Command Prompt”. Возможно, вам потребуется установить средства разработки C/C++ в Visual Studio.
link.exe /debug:full /subsystem:console zerosnake.obj /entry:__managed__Main
Имя символа __managed__Main
является контрактом с компилятором — это имя управляемой точки входа программы, созданной ILC. Но команда не работает:
error LNK2001: unresolved external symbol RhpPInvoke
error LNK2001: unresolved external symbol SetConsoleTextAttribute
error LNK2001: unresolved external symbol WriteConsoleW
error LNK2001: unresolved external symbol GetStdHandle
...
fatal error LNK1120: 17 unresolved externals
Некоторые из этих символов кажутся знакомыми: компоновщик не знает, где искать вызываемые API Windows. Добавим библиотеки для них:
link.exe /debug:full /subsystem:console zerosnake.obj /entry:__managed__Main kernel32.lib ucrt.lib
Выглядит лучше, всего 4 неразрешенных символа:
error LNK2001: unresolved external symbol RhpPInvoke
error LNK2001: unresolved external symbol RhpPInvokeReturn
error LNK2001: unresolved external symbol RhpReversePInvoke2
error LNK2001: unresolved external symbol RhpReversePInvokeReturn2
fatal error LNK1120: 4 unresolved externals
Остальные отсутствующие символы — это помощники, которые компилятор ожидает найти в библиотеке времени выполнения. Их отсутствие обнаруживается только во время связывания, потому что эти помощники обычно реализуются в сборке, и компилятор ссылается на них только по их символическому имени в отличие от других типов и методов, необходимых компилятору и предоставленных нами выше.
Помощники добавляют и удаляют кадры стека, когда машинный код вызывается управляемым или управляемый — машинным. Это необходимо, чтобы работала сборка мусора. Поскольку у нас нет GC, давайте заглушим их кодом C# и другим волшебным атрибутом, который поймёт наш компилятор:
namespace System.Runtime
{
/* Пользовательский атрибут для экспорта метода под заданным символьным именем. */
internal sealed class RuntimeExportAttribute : Attribute
{
public RuntimeExportAttribute(string entry) { }
}
}
namespace Internal.Runtime.CompilerHelpers
{
class StartupCodeHelpers
{
// Тип содержимого для этих методов не важен.
[System.Runtime.RuntimeExport("RhpReversePInvoke2")]
static void RhpReversePInvoke2(System.IntPtr frame) { }
[System.Runtime.RuntimeExport("RhpReversePInvokeReturn2")]
static void RhpReversePInvokeReturn2(System.IntPtr frame) { }
[System.Runtime.RuntimeExport("RhpPInvoke")]
static void RhpPinvoke(System.IntPtr frame) { }
[System.Runtime.RuntimeExport("RhpPInvokeReturn")]
static void RhpPinvokeReturn(System.IntPtr frame) { }
}
}
После перестроения исходного кода с этими изменениями и повторного запуска ILC, связывание, наконец, будет успешным. Мы сейчас на 27 килобайтах. Игра работает!
Возня с линкером
Оставшиеся килобайты можно получить с помощью трюков, которые используют разработчики нативного кода, чтобы уменьшить свои приложения. Мы собираемся:
- Отключить инкрементное связывание.
- Обрезать информацию о релокации.
- Объединить похожие разделы внутри исполняемого файла.
- Установить внутреннее выравнивание в небольшое значение
link.exe /debug:full /subsystem:console zerosnake.obj /entry:__managed__Main kernel32.lib ucrt.lib /merge:.modules=.rdata /merge:.pdata=.rdata /incremental:no /DYNAMICBASE:NO /filealign:16 /align:16
8176 байт! Игра все еще работает и, что интересно, она полностью отлаживаема. Вы можете отключить оптимизацию в ILC, чтобы сделать исполняемый файл еще более отладочным: просто удалите аргумент --Os
.
Ещё меньше?
Исполняемый файл ещё содержит несущественные данные — компилятор ILC просто не предоставляет параметры командной строки, отключающие их генерацию.
Одна из этих избыточных структур данных — информация GC для отдельных методов. В CoreRT есть точный сборщик мусора, требующий, чтобы каждый метод описывал, где находятся ссылки на кучу GC в каждой инструкции тела метода.
Поскольку у нас нет сборщика мусора, эти данные не нужны. Другие среды выполнения — например Mono — используют консервативный сборщик, не требующий этих данных. Он просто предполагает, что любая часть стека и регистров процессора может быть ссылкой GC. Консервативный сборщик торгует производительностью GC ради экономии размера. Точный сборщик CoreRT также может работать в консервативном режиме, но он еще не подключен. Это потенциальное будущее дополнение, которое мы могли бы использовать, чтобы сделать программу ещё меньше. Может, однажды мы сможем сделать упрощенную версию нашей игры в 512 байт загрузочного сектора. А до тех пор — счастливого кода!
Читайте также:
- 4 golang-сниппета, которые вводят в заблуждение разработчиков C#!
- Эффективное использование словаря (C#) как альтернатива оператору If
- 10 правил программирования NASA
Перевод статьи Michal Strehovský: Building a self-contained game in C# under 8 kilobytes
I have this simple snake game, my problem is that the tails wont add when it reaches three tails.
namespace Snake
{
public partial class Form1 : Form
{
bool left = false, right = false;
bool top = false, down = false;
PictureBox pic = new PictureBox();
List<PictureBox> tails = new List<PictureBox>();
int score = 0;
public Form1()
{
InitializeComponent();
}
private void Form1_KeyPress(object sender, KeyPressEventArgs e)
{
if (((e.KeyChar.ToString() == "a") || (e.KeyChar.ToString() == "A"))&&(right == false))
{
right = false;
top = false;
down = false;
left = true;
}
else if (((e.KeyChar.ToString() == "d") || (e.KeyChar.ToString() == "D"))&& (left == false))
{
top = false;
down = false;
left = false;
right = true;
}
else if (((e.KeyChar.ToString() == "w") || (e.KeyChar.ToString() == "W"))&& (down == false))
{
down = false;
left = false;
right = false;
top = true;
}
else if (((e.KeyChar.ToString() == "s") || (e.KeyChar.ToString() == "S"))&& (top == false))
{
top = false;
left = false;
right = false;
down = true;
}
}
private void timer1_Tick(object sender, EventArgs e)
{
//ticks every 1 sec
if (pic.Location == head.Location)
{
score++;
spawnFood();
tails.Add(addTails());
}
sortLocation();
if (right == true)
{
int r = head.Location.X + head.Height;
head.Location = new Point(r, head.Location.Y);
}
else if(left == true)
{
int l = head.Location.X - head.Height;
head.Location = new Point(l, head.Location.Y);
}
else if (top == true)
{
int t = head.Location.Y - head.Height;
head.Location = new Point(head.Location.X, t);
}
else if (down == true)
{
int d = head.Location.Y + head.Height;
head.Location = new Point(head.Location.X,d);
}
txtScore.Text = score.ToString();
}
private void sortLocation()
{
if (tails.Count == 0)
{
}
else
{
for (int i = 1; i < tails.Count; i++)
{
tails[i].Location = tails[i-1].Location;
}
tails[0].Location = head.Location;
}
}
private PictureBox addTails()
{
PictureBox tail = new PictureBox();
tail.Name = "tail" + score.ToString();
tail.BackColor = Color.Black;
tail.Width = 10;
tail.Height = 10;
this.Controls.Add(tail);
return tail;
}
private void spawnFood()
{
Random rnd = new Random();
int rndLocationX = rnd.Next(10, 50);
int rndLocationY = rnd.Next(10, 50);
pic.BackColor = Color.Red;
pic.Height = 10;
pic.Width = 10;
this.Controls.Add(pic);
if (rndLocationX >= 500)
{
rndLocationX -= 10;
}
if (rndLocationY >= 500)
{
rndLocationY -= 10;
}
pic.Location = new Point(rndLocationX*10,rndLocationY*10);
}
private void Form1_Load(object sender, EventArgs e)
{
timer1.Start();
spawnFood();
}
}
}