Логин: Пароль:    Регистрация Всеми возможностями сайта можно пользоваться
только после авторизации.
   Забыли пароль?
Поиск
L


Статистика
u
Пользователи онлайн: нет
Гостей онлайн: 5
Всего онлайн: 5
Зарегистрировано юзеров: 5762
Комментариев на сайте: 623
Новый юзер: Sevnday



Последние комментарии
c
N0E0O7 прокомментировал "Урок 1 - Инициализация OpenGL":
А у меня форма чёрная запускается
dimonsky прокомментировал "Урок 2 - Простые примитивы":
GetDC(handle) вместо canvas.handle, и убрать вызов FormResize. Тогда получается конфетка.



Мы в соцсетях
c
Delphi
Что такое "Змейка" и как с ней бороться?

Введение
Сразу отвечу на первый вопрос. "Змейка" это игра, в которой вы управляете полоской, набирая очки. Цель у игры одна: набрать наибольшее количество очков.

Как-то увидел я на сотовом телефоне эту игру, и уж очень мне захотелось написать точно такую же. В этой статье хочу рассказать, как написать "змейку". С какими проблемами можно столкнуться при создании "змеек". И попробую рассказать, как я их решал.

Сразу хочу сказать, что я пишу свою статью не для того, чтобы написать "Змейку", а только для объяснения алгоритма работы.

Поле действии "змейки" - массив
Начнем с того, что нам надо придумать то, по чем наша змейка будет двигаться. Лично я вижу всего два способа. Один - это когда змейка движется по абсолютно пустому пространству. Второй же способ - массив. Именно этот способ я выбрал в своем примере. Мне кажется, что при использовании массива открываются некоторые довольно приятные возможности, такие как создание карт, и, вместе с тем, при создании многопользовательской змейки будет удобнее делать проверки.

Из чего состоит "змейка"?
Прежде чем приступать к внутренности "змейки" давайте посмотрим как она движется.

Движение змейки можно рассмотреть с двух сторон:

Первая - это то, что конец змейки становится началом. Плюсы: нам легче будет прорисовывать. Нам не надо постоянно перерисовывать весь массив, нам надо всего лишь затереть конец змейки и нарисовать начало. Такой метод будет очень удобен при работе в DOS.

Минусы: у нас возникают проблемы с опознаванием первого и последнего элементов, а добавление нового элемента тоже будет немало проблем, при какой-либо ошибку у нас может потерятся часть змейки, поверьте в прошлом году при написании "змейки" на Pascal'е у меня было очень много таких "глюков", причем только в многопользовательской игре.

Вторая сторона - каждый элемент змейки становится на место предыдущего. Плюсы: мы всегда знаем, где какой элемент; добавление и удаление будут делаться буквально несколькими строками в коде; у нас ни в коем случае не может потеряться элемент, т.к. мы всегда знаем все.

Минусы: нам надо больше места в ОП для сортирования всех этих элементов.

Первый вариант, конечно же, проще, но при его использовании мы столкнемся с большим количеством проблем оговоренных выше. Второй вариант можно реализовать двумя способами (может и больше, но я не вижу других).

Первый - создать большой массив для змеи и хранить в нем все координаты. Плюсы: плюс один - простота.

Минусы: а когда у нас появится две змейки нам что делать два массива? а три? а четыре? Нет это совершенно не подходит, это занимает много места в ОП, и мы ограниченны длинной змейки. Существуют конечно функции для задания массива SetLength, но при ее использовании появляется ограничение на длину массива, всего 255.

Второй - создать динамическую структуру и в ней хранить все данный о конкретном элементе змейки. Плюсы: нам не надо большого количества памяти, мы под каждый новый элемент будем выделять еще память.

Минусы: нам надо будет больше писать (конечно это не самое главное, главное чтобы пользователь был доволен)

Не знаю как вам, а мне второй вариант больше нравится, потому его и реализуем.

Что нам стоит "змею" построить?
Мы уже решили, что элементы нашей "змейки" будут представлять динамическую структуру, тогда элемент змейки будет выглядеть следующим образом:

	type
	  TElem = ^TStek;	//объект не может ссылаться сам на себя
	  TStek = record
	    X: integer;		//координата по X 
	    Y: integer;		//координата по Y
	    View: Byte;		//вид этого элемента при прорисовке
	    Nomber: Word;	//номер элемента
	    Next: TElem;	//следующий элемент змейки
	    Prev: TElem;	//предыдущий элемент змейки
	  end;
	
Сама же змейка будет выглядеть следующим образом - это объект, у которого есть свойства цвет (много змеек - много цветов), длина (в принципе этот параметр можно не вводить, можно узнать длину по номеру последнего элемента, но пусть будет), направление (очень важный параметр, мы же должны знать, куда движется "змейка").

Направлений у нас будет всего 4 соответственно
	TDir = 1..4; //образованно от Direction

	TSnake = object
	  First: TElem;		//первый элемент змейки
	  Last: TElem;		//последний элемент змейки
	  Direction: TDir;	//направление змейки
	  Length: Word;		//длина змейки
	  Color: TColor;		//цвет змейки
	  constructor Create(sDirection: TDir; sLength: Word; sX, sY: Byte; sColor: TColor);
	  //процедура создания змеи
	  destructor Destroy;
	  //процедура уничтожения змеи

	  procedure Release;
	  //процедура, выводящая всю змею на массив (чтобы знать где расположена змея, вдруг
	  //будем вставлять алгоритм поиска пути или еще что в голову придет)

	  procedure Draw(Canvas: TCanvas); 
	  //отрисовывает змею на канвасе (на канвасе, потому, что пример не показывает работу
	  //с Win32API

	  procedure DrawElement(Elem: TElem; Canvas: TCanvas); 
	  //отрисовывает определенный элемент змеи

	  procedure Move(newX, newY: word); 
	  //двигает все элементы змейки на место предыдущего, а первый на новые координаты
	  //можно с помощью этой процедуры двинуть змейку вообще в какое-нибудь случайное
	  //место, а все элементы потом "телепортируются" туда
	  procedure Add;                     
	  //добавляет к змейке элемент (последний)
	  procedure Remove;              
	  //уничтожает элемент змейки (последний)

	  function GetByNomber(Nomber: Word): TElem; 
	  //находит элемент по номеру
	  function GetByCoord(X, Y: word): TElem;         
	  //находит элемент по координатам

	  //Вдруг да понадобится найти элемент с определенным номером или расположением

	  function SetDirection(sDirection: TDir): boolean;
	  //Устанавливает направление змейки
	  //Делается функцией, чтобы змейка не поехала в противоположную сторону

	  procedure SetView(sView: byte);  
	  //Устанавливает вид всей змеи (ну захотелось все змею сделать круглой - сделай!!!)
	end;
Наконец код
Функция создания змейки

При создании мы должны установить цвет, направление, длину, стартовые координаты. Чтобы сильно не мучаться, моя змейка создается в горизонтальном положении, а в процедуре задаются координаты конца.
	constructor TSnake.Create(sDirection: TDir; sLength: Word; sX, sY: Byte; sColor: TColor);
	var
	  i: integer;
	  Beg, Tek, Tek2: TElem;
	begin
	  if sDirection = 3 then Direction := 1 else Direction := sDirection;
	  Length := sLength;
	  Color := sColor;

	  First := nil;
	  Last := nil;

	//делаем проверку на правильное введение координат
	//проверяем, что не вышли за рамки массива
	  if (sX<1) or (sX>AMaxX) or (sY<1) or (sY>AMaxY) or ((sX+sLength)>AMaxX) then
	  begin
	    ShowMessage('Ошибка создания змейки');
	    exit
	  end;

	  if sLength=0 then begin ShowMessage('Ошибка создания змейки'); exit end;
	  new(Beg); //создаем первый элемент змейки

	  Beg^.X := sX + sLength; 	//координата по X - координата конца + длинна
	  Beg^.Y := sY;		 	//по Y все координаты одинаковые
	  Beg^.View := 0;		//вид самый простой
	  Beg^.Nomber := 1;		//номер естественно первый
	  Beg^.Next := nil;		//следующего элемента пока нет
	  Beg^.Prev := nil;		//предыдущего тоже

	  first := beg;			//запомнили первый элемент змейки
					//с этого момента в змейке есть первый элемент уррра!!! 

	  Tek := beg;			//текущий элемент - первый
	  if sLength-1 < 1 then exit;	//если длина 1 тогда ВСЕ


	  for i := 1 to (sLength-1) do begin //иначе начинаем

	    new(Tek2);  			//создаем новый элемент

	    Tek2^.X := Tek^.X - 1; 		//располагаем новый элемент левее
	    Tek2^.Y := Tek^.Y;   		//по оси Y все находятся одинаково
	    Tek2^.View := 0;     		//стартовый вид у всех одинаковый
	    Tek2^.Next := nil;   		//следующий элемент должен быть тот, который мы до этого 
	//создали
	    Tek2^.Prev := Tek;   		// предыдущего пока не существует

	    Tek2^.Nomber := Tek^.Nomber + 1; //прибавляем номер

	    Tek^.Next := Tek2;		//предыдущему ставим существование следующего
	    Tek := Tek2;			//текущий - вновь созданный
	    Last := Tek2;			//последний созданный элемент - это и есть последний 
	//элемент змейки
	  end;
	end;
Вот мы и создали нашу змейку. Теперь надо написать процедуру уничтожения, чтобы в памяти не осталось следов.
	destructor TSnake.Destroy;
	var Tek, Tek2: TElem;
	begin
	  Tek := First;
	  if tek = nil then exit; 		//вдруг змеи нет

	  while tek^.Next <> nil do begin
	    Tek2 := Tek.Next;
	    dispose(tek2);			//уничтожаем
	    tek := tek2;
	  end;
	end;
Далее рассмотри, как рисовать змейку.
	procedure Tsnake.DrawElement(Elem: TElem; anvas: TCanvas);
	var Rect: TRect;
	begin
	  Rect.Left := Elem.X * 5;
	  Rect.Top := Elem.Y * 5;
	  Rect.Bottom := Rect.Top + 5;
	  Rect.Right := Rect.Left + 5;

	  Canvas.Pen.Color := clBlack;
	  Canvas.Brush.Color := Color;
	  case Elem.View of
	  0: Canvas.Rectangle(Rect); 	//тут мы можем сделать и что-нибудь другое, например
	//кружочек вместо квадрата в зависимости от вида
	  end;
	end;
А теперь самое интересное. Рассмотрим функцию движения.

В змейке движение происходит следующим образом: Первый элемент передвигается согласно текущему направлению, а второй становится на место первого, третий на место второго и т.д. Значит, каждый элемент занимает место предыдущего кроме первого. Ну, так давайте это реализуем.
	function TSnake.Move(newX, newY: word): boolean;
	var 
	  tek: TElem;
	  tek2: TElem;
	begin
	  result := false;
	  if First = nil then exit;

	  tek := last;

	  if newX > AMaxX then newX := 1;
	  if newX < 1 then newX := AMaxX;
	  if newY > AMaxY then newY := 1;
	  if newY < 1 then newY := AMaxY;

	  Release;

	  if GameA[newX, newY] > 0 then result := true;

	  while tek^.Prev <> nil do begin
	    tek2 := tek^.Prev;		//предыдущий элемент
	    tek^.X := tek2^.X;		//координаты предыдущего
	    tek^.Y := tek2^.Y;		//элемента
	    tek^.View := tek2^.View;	//вид предыдущего
	    tek :=tek2;			//заново
	  end;

	  First^.X := newX;		//перемещаем начало
	  First^.Y := newY;		//на новые координаты
	end;
Далее надо каким-то образом добавлять и удалять у змейки элементы. Все просто надо просто добавить или удалить последние элемент.
	procedure TSnake.Add; //добавление
	var tek: TElem;
	begin

	  new(tek);		//создаем новый элемент
	  tek^.X := 0;		//координаты можно ставить любые,
	  tek^.Y := 0; 		//все равно при движении все встанет на свои места
	  tek^.Next := nil;	//следующего не существует (он последний)
	  tek^.Prev := Last;	//предыдущий - тот, который был последним
	  tek^.View := Last^.View;	//вид, как у предыдущего
	  tek^.Nomber := Last^.Nomber + 1;	//номер +1
	  Last^.Next := tek;	//новый последний элемент змейки

	  inc(length);		//незабываем увеличить длину

	  Last := Tek;

	end;

	procedure TSnake.Remove; //удаление
	var Tek: TElem;
	begin
	  if Last = First then exit; 	//не можем же мы удалить единственный элемент
					//что тогда у нас останется?
	  Tek := Last;
	  Last := tek^.Prev;
	  Last^.Next := nil;
	  Dispose(tek);

	  inc(length, -1);
	end;
Остальные функции и процедуры посмотрите в исходнике, т.к. они нам не понадобятся.

Создав форму в событии OnCreate, cтавим
	//обнуляем массив
	for i := 1 to AMaxX do
	for j := 1 to AMaxY do
	GameA[i, j] := 0;

	Buffer := TBitMap.Create;
	Buffer.Width := Form1.Width;
	Buffer.Height := Form1.Height;

	//создаем змейку
	Snake.Create(1, 5, 10, 10, clYellow);
	В событии OnDestroy пишем

	Snake.Destroy;
	В событии на таймер ставим

	//очистка канваса
	PatBlt(Buffer.Canvas.Handle,
	0,
	0,
	Buffer.Width,
	Buffer.Height,
	BLACKNESS);

	//согласно направлению двигаем змейку (на самом деле
	//это можно и организовать и в функцию Move, но я решил оставить так
	case Snake.Direction of
	1: Snake.Move(Snake.First.X + 1, Snake.First.Y);
	2: Snake.Move(Snake.First.X, Snake.First.Y + 1);
	3: Snake.Move(Snake.First.X - 1, Snake.First.Y);
	4: Snake.Move(Snake.First.X, Snake.First.Y - 1);
	end;

	Snake.Draw(Buffer.Canvas);

	BitBlt(Form1.Canvas.Handle, 0, 0, Form1.Width, Form1.Height, Buffer.Canvas.Handle,
	0, 0, SRCCOPY); //выводи на форму


Вот и всё, Удачи!

Источник: www.thedelphi.ru
Автор: Савельев Александр
Опубликовано: 29 Октября 2013
Просмотров: 110071


Зарегистрируйтесь или авторизуйтесь, чтобы добавлять комментарии.