Пишу змейку в IntellijIdea на java. Подскажите, пожалуйста, почему может не работать ни repaint(), ни update(g), ни updateUI() [закрыт]
Вопросы с просьбами помочь с отладкой («почему этот код не работает?») должны включать желаемое поведение, конкретную проблему или ошибку и минимальный код для её воспроизведения прямо в вопросе. Вопросы без явного описания проблемы бесполезны для остальных посетителей. См. Как создать минимальный, самодостаточный и воспроизводимый пример.
Закрыт 2 года назад .
Змейка ползёт только если сворачивать и разворачивать окно ( А так стоит на месте. Может ли это быть связано с настройками самой IntelliJIdea, или проблема в моей программе? Вот код:
import javax.swing.*; import java.awt.*; import java.util.ArrayList; public class Snake extends JPanel implements Runnable < public static int length; public static ArrayListparts; public static Field field; public static Food food; public static int speedX=0; public static int speedY=1; //private Graphics g; private static Snake snake = null; public static final int TIME_DELTA = 500; public static Snake getSnake(int w, int h) < if(snake == null) snake = new Snake(w, h); return snake; >private Snake(int Width, int Heigth) < super(true); food = new Food(); field = Field.getField(); Point start = new Point((int)Width/2, (int)Heigth/2); //размеры поля, а не окна parts = new ArrayList<>(); parts.add(start); Point p1 = new Point((int)start.getX(), ((int)start.getY())-1); parts.add(p1); Point p2 = new Point((int)start.getX(), ((int)p1.getY())-1); parts.add(p2); length = 3; // paint(g); > public static void move() < for (Point i: parts) < i.y-=1*speedY; i.x-=1*speedX; >> public static void eat() < Point np = new Point ((int)parts.get(length).getX(),(int)parts.get(length).getY()-1 ); parts.add(np); ++length; food.respawn(); >public static boolean checkFood() < if(parts.get(parts.size()-1).getX() == food.x && parts.get(parts.size()-1).getY()==food.y) return true; else return false; >public static boolean checkHead() < for (int i=1; iif(parts.get(parts.size()-1).getX() = field.sizeX || parts.get(parts.size()-1).getY() = field.sizeY ) return false; return true; > @Override public void paint(Graphics g) < super.paint(g); for (Point i: parts) g.fillRect((int) i.getX() * 10, (int) i.getY() * 10, 8, 8); g.setColor(Color.RED); g.fillRect(food.x * 10, food.y * 10, 8, 8); g.setColor(Color.BLACK); >@Override public void run() < while (checkHead()) < move(); repaint(); if(checkFood()) eat(); try < Thread.sleep(TIME_DELTA); >catch (InterruptedException e) < e.printStackTrace(); >> > >
import javax.swing.*; import java.awt.*; public class Window extends JPanel < private Graphics g; private Snake snake; public Window() < super(true); snake = Snake.getSnake(50, 50); Thread snakeThread = new Thread(snake); snakeThread.start(); >@Override public void paint(Graphics g)
import java.awt.*; public class Field < public static int sizeX, sizeY; public static int[][] coordinates; public static Field field = null; public static Field getField() < if(field == null) field = new Field(500, 500); return field; >private Field(int x, int y) < sizeX=x/10; sizeY=y/10; coordinates = new int[sizeX][sizeY]; for (int i=0; i> > public static void changeField(int x, int y, boolean SetOrDel)// т - добавить, иначе убрать < if (SetOrDel) < coordinates[x][y]=1; >else < coordinates[x][y]=0; >>
import javax.swing.*; import java.awt.*; import java.util.Random; public class Food extends JPanel < public static int x; public static int y; private static Random random; public Food() < super(true); random = new Random(); x = random.nextInt(50); y = random.nextInt(50); >@Override public void paint(Graphics g) < super.paint(g); g.setColor(Color.RED); g.fillRect(x * 10, y * 10, 8, 8); g.setColor(Color.BLACK); >public void respawn() < x = random.nextInt(40); y = random.nextInt(40); repaint(); >>
Поменяла конструктор, ничего не изменилось(. Он же не срабатывает при сворачивании и разворачивании окна?
private Snake(int Width, int Heigth) < super(true); food = new Food(); field = Field.getField(); Point start = new Point((int)Width/2, (int)Heigth/2); //размеры поля, а не окна parts = new ArrayList<>(); parts.add(start); Point p1 = new Point((int)start.getX(), ((int)start.getY())-1); parts.add(p1); Point p2 = new Point((int)start.getX(), ((int)p1.getY())-1); parts.add(p2); length = 3; timer = new Timer(TIME_DELTA, null); timer.addActionListener(new ActionListener() < @Override public void actionPerformed(ActionEvent e) < if(checkHead() ) < move(); repaint(); if (checkFood()) eat(); >> >); timer.start(); >
Java/Игра змейка
Если не терпится написать что-нибудь интересное, например игру — вы находитесь на правильной странице! Данная игра не является законченным продуктом, в ней почти нет коментариев, зато она работает и в ней есть несколько ключевых элементов с помощью которых можно написать свою собственную игру. В общем, берем исходники и доделываем/переделываем игру сами!
Дизайн программы [ править ]
На графическом поле со стенами ползает змейка, которой игрок управляет стрелками, собирая апельсины.
Функциональность [ править ]
//**//Для начала в объекта actions установим голову змейки в центр поля. Потом создаем 2 массива в которых будут храниться координаты //**//сегментов змейки. //**// Действия которые выполняются при загрузке объекта onClipEvent (load) //**//Переменная в которой хранится количество сегментов тела змейки body_count = 0; //**//Переменная необходимая для паузы в движении змейки move_timer=getTimer(); //**//Переменная необходимая для паузы при создании бонусов bonus_timer=getTimer(); //**//Переменная которая указывает создан ли бонус или нет. bonus_exist = 0; //**//Устанавливаем голову змейки в центр игрового поля _root.snake_head._x = 290; _root.snake_head._y = 250; //**//Создаем массивы в которых будем хранить координаты тела змейки var cell_x:Array = new Array(); var cell_y:Array = new Array(); //**// Затем методом дубликации создадим начальный хвост для змейки, и вписываем координаты каждого сегмента в массивы. //**//Создание хвоста змейки циклом от 1 до 7 for (i=1; i7; i++) duplicateMovieClip(_root.snake_body, 'snake_body'+i, i); //**//Расстановка сегментов хвоста на поле, снизу головы змейки _root['snake_body'+i]._x = _root.snake_head._x; _root['snake_body'+i]._y = _root.snake_head._y+(i*20); //**//Вписываем в массив новые переменные cell_x[i+1] = _root['snake_body'+i]._x; cell_y[i+1] = _root['snake_body'+i]._y; //**//Записываем в переменной количество сегментов тела body_count++; >> //**//Затем добавим код, который будет управлять движением змейки, он очень прост, при нажатии на кнопку управления - голова змейки //**//меняет свое направление. //**//Блок управления движением змейки при нажатии кнопки - змейка меняет направление on (keyPress "") _root.snake_head._rotation = -90; > on (keyPress "") _root.snake_head._rotation = 90; > on (keyPress "") _root.snake_head._rotation = 0; > on (keyPress "") _root.snake_head._rotation = 180; > //**//Для движения змейки, появления бонусов, проверки столкновений, испольуем событие которое будет проверяться каждый кадр //**//мувика(onClipEvent (enterFrame)). Напишем код который будет дублировать мувик бонуса и размещать его в случайном месте на сцене, //**//перед дубликацией мувика проверяется тайме и переменная которая указывает создан бонус или нет, это нужно для того чтобы на //**//сцене размещался только 1 бонус. onClipEvent (enterFrame) //**//Создание бонусов. //**//Проверяем, если таймер меньше и переменная указывает что бонуса не создано - создаем бонус. if (getTimer()-bonus_timer>500 && !bonus_exist) //**//Дублируем клип, присваиваем ему имя duplicateMovieClip(_root.bonus, 'bonus_real', 1000); //**//Устанавливаем случайные координаты для дублированного бонуса _root.bonus_real._x = random(30)*20+10; _root.bonus_real._y = random(25)*20+10; //**//Ставим в переменной что бонус создан. bonus_exist = 1; > //**//Движение тела змейки по клеткам с проверкой таймера, для создания задержки и вписывание новых данных в массивы //**//Движение змейки //**//Проверяем таймер, для движения с задержкой. if (getTimer()-move_timer>170) //**//Обновление таймера движения. move_timer = getTimer(); //**//вписываем в массив координаты головы змейки cell_x[1] = _root.snake_head._x; cell_y[1] = _root.snake_head._y; //**//Движение головы змейки в клетку по направлению(_rotation) _root.snake_head._x += 20*Math.sin(_root.snake_head._rotation*(Math.PI/180)); _root.snake_head._y -= 20*Math.cos(_root.snake_head._rotation*(Math.PI/180)); //**//Запись циклом сегментов тела змейки в массивы for (i=1; ibody_count+1; i++) _root['snake_body'+i]._x = cell_x[i]; _root['snake_body'+i]._y = cell_y[i]; > //**//Передвижение циклом всех сегментов тела змейки в вышестоящую ячейку массива. for (i=1; ibody_count+1; i++) cell_x[i+1] = _root['snake_body'+i]._x; cell_y[i+1] = _root['snake_body'+i]._y; > //**//Проверку столкновения головы змейки с бонусом, осуществяется встроенной функцией Flash - hitTest(), если столкновение произошло //**//- дублируем для тела змейки новые сегменты и устанавливаем им позицию в хвосте змейки на место последнего сегмента, при //**//последующем движении змейки - они будут следовать за ней, и вписывать свои координаты в массивы. //**//Проверка столкновения головы змейки с бонусом методом HitTest`а и добавление новых сегментов к телу змейки if (_root.snake_head.hitTest(_root.bonus_real._x, _root.bonus_real._y)) trace('Bonus eat, you grow!'); removeMovieClip(_root.bonus_real); //**//Устанавливаем переменную, что бонус съеден и обновляем таймер для создания нового бонуса bonus_exist = 0; bonus_timer = getTimer(); //**//Добавляем циклом новые сегменты тела к змейке на последний сегмент тела и увеличиваем переменную отвечающую за длину тела. for (i=0; i4; i++) duplicateMovieClip(_root.snake_body, 'snake_body'+body_count, body_count); body_count++; _root['snake_body'+body_count]._x = _root['snake_body'+(body_count-1)]._x; _root['snake_body'+body_count]._y = _root['snake_body'+(body_count-1)]._y; cell_x[body_count] = _root['snake_body'+body_count]._x; cell_y[body_count] = _root['snake_body'+body_count]._y; > > //**//Проверка столкновения головы змейки с телом осуществляется циклом с функцией hitTest(), циклом проверяются все сегменты змейки //**//на столкновение с головой. Если столкновение произошло - функцией fscommand осуществляется закрытие мувика. //Проверка столкновения головы с сегментами тела HitTest`ом for (i=0; ibody_count+1; i++) //**//Если столкновение произошло - выводим сообщение и закрываем мувик. if (_root.snake_head.hitTest(_root['snake_body'+i]._x, _root['snake_body'+i]._y, 0)) trace('You eat your body. Cannibal !'); fscommand('quit', true); > > > >
Классы [ править ]
- SnakeApp — Окно игры. Окно игры содержит игру, слушает клавиатуру и пересылает команды
- SnakeGame — Главный класс игры. В сущности вся игра тут
- Snake — класс змейки. Змейка вынесена в отдельный класс, чтобы в будущем была возможность создания нескольких экземпляров одновременно
Отладка [ править ]
Так как программа сырая, вам предлагается ее улучшить и доделать. Вот неполный список вещей, которые нужно исправить:
- Нет проверок на самосъедание
- После аварии змейка может продолжить движение (уйти в бесконечность)
- Нет возможности перезапустить игру не закрывая окна
Исходники [ править ]
- SnakeApp.java — Окно игры
- SnakeGame.java — Главный класс игры
- Snake.java — класс змейки
- sn1.dat — текстовый файл с данными о стенках (поместите его в C:\ или измените путь в классе SnakeGame или в папке NetBeans scr. )
Как написать свою змейку на Java за 15 минут
В предыдущей статье мы писали сапёра за 15 минут, теперь займёмся классической змейкой.
В этот раз нам снова понадобятся:
- 15 минут свободного времени;
- Настроенная рабочая среда, т.е. JDK и IDE (например Eclipse);
- Библиотека LWJGL (версии 2.x.x) для работы с Open GL. Обратите внимание, что для LWJGL версий выше 3 потребуется написать код, отличающийся от того, что приведён в статье;
- Спрайты, т.е. картинки самой змеи и фрукта, который она будет есть. Можно чисто символически нарисовать самому, или скачать использовавшиеся при написании статьи.
Подключение библиотек
В прошлый раз у многих возникли с этим вопросом проблемы, поэтому мне показалось уместным посвятить этому немного времени. Во-первых, выше я дал ссылку на скачивание архива с библиотеками, которые использую я, чтобы не было путаницы с версиями и вопросов, где найти что-то. Папку из архива требуется поместить в папку проекта и подключить через вашу IDE.
Во-вторых, у многих пользователей InteliJ IDEA возникли проблемы как раз с их подключением. Я нашёл в сети следующий видеогайд:
После того, как я сделал всё в точности по нему, у меня библиотеки подключились корректно и всё заработало.
Работа с графикой
С этой стороны наша задача мало отличается от той, что мы выполняли при написании Сапёра. Снова создаём класс GUI, который будет хранить и обновлять состояние всех графических элементов. Если точнее:
- Класс будет выполнять инициализацию OpenGL:
initializeOpenGL()
///Class GUI private static void initializeOpenGL() < try < //Задаём размер будущего окна Display.setDisplayMode(new DisplayMode(SCREEN_WIDTH, SCREEN_HEIGHT)); //Задаём имя будущего окна Display.setTitle(SCREEN_NAME); //Создаём окно Display.create(); >catch (LWJGLException e) < e.printStackTrace(); >glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0,SCREEN_WIDTH,0,SCREEN_HEIGHT,1,-1); glMatrixMode(GL_MODELVIEW); /* * Для поддержки текстур */ glEnable(GL_TEXTURE_2D); /* * Для поддержки прозрачности */ glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); /* * Белый фоновый цвет */ glClearColor(1,1,1,1); >
Как вы можете видеть, здесь я уже использовал несколько констант. Для них был создан отдельный класс Constants с public static полями. Вот он целиком:
public class Constants < ///Размер игровой ячейки public static final int CELL_SIZE = 32; ///Размеры игрового поля в ячейках public static final int CELLS_COUNT_X = 20; public static final int CELLS_COUNT_Y = 20; ///Шанс появления ягод на старте в процентах. ///При выставленном значении спавнится 3-5 ягод. ///Не беспокойтесь, что значение слишком низкое, как минимум одна ягода создаётся отдельно. public static final int INITIAL_SPAWN_CHANCE = 1;//% ///В нашем случае змея проходит одну клетку за один фрейм. ///Значение 5 мне показалось оптимальным, но вы можете экспериментировать. public static final int FPS = 5; ///Константы для создания окна, названия достаточно говорящие. public static final int SCREEN_WIDTH =CELLS_COUNT_X*CELL_SIZE; public static final int SCREEN_HEIGHT = CELLS_COUNT_Y*CELL_SIZE; public static final String SCREEN_NAME = "Tproger's Snake"; >
Enum Sprite , который отвечает за подгрузку текстур, полностью идентичен тому, что мы писали для Сапёра, за исключением того, что нам нужно только две текстуры — для змеи и для ягод. Вот код:
public enum Sprite < ///Файлы с именами circle и cherries должны лежать по адресу /// %папка проекта%/res/ в расширении .png BODY("circle"), CHERRIES("cherries"); private Texture texture; private Sprite(String texturename)< try < this.texture = TextureLoader.getTexture("PNG", new FileInputStream(new File("res/"+texturename+".png"))); >catch (IOException e) < e.printStackTrace(); >> public Texture getTexture() < return this.texture; >>
Механика игры
Самое время поговорить о том, как наша змея будет, собственно, перемещаться. Вам наверняка доводилось видеть вывески, вокруг которых по кругу бегают огоньки? Разумеется, сами лампочки в них не перемещаются, просто каждый тик последняя гаснет, а первая зажигается. Таким же образом будет перемещаться и наша змея.
Несложно подсчитать, что каждая лампочка должна гореть столько тиков, какова длина “змеи”. Значит, мы должны сообщить клетке, в которую попадает змея, что она должна гореть определённое количество секунд, а каждый тик уменьшать это число у каждой клетки с ненулевым таймером, и менять спрайт, если змея из клетки уже выползла (т.е. таймер стал равен нулю). В случае же необходимости удлинить цепочку, достаточно просто не уменьшать время “горения” клеток на каком-то тике. Именно поэтому метод update() у классов Cell и GUI принимает параметр — если он равен false , значит, змея что-то съела.
Пишем класс клетки
public class Cell < private int x; private int y; private int state;/* 0 ->ячейка пуста >0 -> в ячейке тело змеи, которое будет там ещё N фреймов Что-то необычное: -1: Ягоды */ ///Конструктор просто выставляет начальные значения координат и состояния public Cell (int x, int y, int state) < this.x=x; this.y=y; this.state=state; >///==== Ничем не примечательные геттеры и сеттеры public int getX() < return x; >public int getY() < return y; >public int getHeight() < return CELL_SIZE; >public int getWidth() < return CELL_SIZE; >public int getState() < return this.state; >public void setState(int state) < this.state = state; >///==== ///Метод обновления клетки. Уменьшаем время "горения", если это необходимо public void update(boolean have_to_decrease) < if (have_to_decrease && this.state >0) < this.state--; >> ///Ячейка "думает" как она должна выглядеть public Sprite getSprite() < if(this.state >0)< ///Если в ней тело змеи -- как змея return Sprite.BODY; >else if(this.state==0)< ///Если в ней нет ничего -- никак выглядеть и не должна return null; >else < ///Иначе проходимся свитчем по возможным объектам. ///Так как это демо -- я добавил только ягоды switch(this.state)< default: return Sprite.CHERRIES; >> > >
Добавляем геттер и сеттер для состояния клетки поля в GUI
getState(x,y) < return cells[x][y].getState(); >setState(x,y,state)
Своя змейка, или пишем первый проект. Часть 0
Привет Хабр! Меня зовут Евгений «Nage», и я начал заниматься программированием около года назад, в свободное от работы время. Просмотрев множество различных туториалов по программированию задаешься вопросом «а что же делать дальше?», ведь в основном все рассказывают про самые основы и дальше как правило не заходят. Вот после продолжительного времени за просмотром разных роликов про одно и тоже я решил что стоит двигаться дальше, и браться за первый проект. И так, сейчас мы разберем как можно написать игру «Змейка» в консоли со своими начальными знаниями.
Глава 1. Итак, с чего начнем?
Для начала нам ничего лишнего не понадобится, только блокнот (или ваш любимый редактор), и компилятор C#, он присутствует по умолчанию в Windows, находится он в С:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe. Можно использовать компилятор последней версии который поставляется с visual studio, он находится Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\Roslyn\csc.exe.
Создадим файл для быстрой компиляции нашего кода, сохранил файл с расширением .bat со следующим содержимым:
@echo off :Start set /p name= Enter program name: echo. С:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.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 ; 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 Listwall = new List(); 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 Listsnake; 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(); 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 ; 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 Listwall = new List(); 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
Теперь можем создать класс змейки, где опишем как она будет ползать, поворачивать. Определим список точек змеи, наше перечисление, шаг на сколько будет перемещаться за ход, и ссылки на хвостовую и головную точки, и конструктор, в котором рисуем змею в заданных координатах и заданной длинны при старте игры.
class Snake < private Listsnake; 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(); 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.
Всем удачи!