#28.1 pascal Практика ООП: анимация и заготовка для консольной игры

В этом уроке мы постараемся привести более "жизненные" примеры использования ООП в простых анимациях и играх

Пример №1 - Перемещение объекта по команде от пользователя -- переход к ООП

Есть код, который, используя обновление консоли как в примере с анимацией, может дать пользователю перемещать символ по экрану влево и вправо:

uses Crt;

procedure drawFrame(skin: char; position: integer);
var i: integer;
begin
  for i:=1 to (position-1) do
    write(' ');
  writeln(skin);
end;

function getNewPosition(position: integer): integer;
var command: char;
begin
  readln(command);
  if (command = '2') then
    result := position + 1
  else
    if (position > 1) then
      result := position - 1;
end;

//  *
var position: integer;
  skin, command: char;
begin
  skin := '*'; // внешний вид
  command := '1'; // последняя введенная команда 
  position := 1; // текущая позиция
  while(command <> 'q') do
  begin
    ClrScr();
    drawFrame(skin, position);
    position := getNewPosition(position);
  end;

  writeln('Программа завершена!');

  readln();
end.

его можно переписать в стиле ООП, например, так:

uses Crt;

type
  Animal = class
    public
      constructor create(skinValue: char);
      procedure drawFrame();
      procedure handleCommand();
      function getCommand(): char;
    protected
      skin: char; // внешний вид
      position: integer; // текущая позиция
      command: char; // последняя введенная команда
      function getNewPosition(commandValue: char): integer;
  end;

constructor Animal.create(skinValue: char);
begin
  self.skin := skinValue;
  self.command := ' ';  // значение по умолчанию
end;

procedure Animal.drawFrame();
var i: integer;
begin
  for i:=1 to (self.position-1) do
    write(' ');
  writeln(self.skin);
end;

procedure Animal.handleCommand();
begin
  readln(self.command);
  self.position := self.getNewPosition(self.command);
end;

function Animal.getNewPosition(commandValue: char): integer;
begin
  if (commandValue = '2') then
    result := position + 1
  else
    if (position > 1) then
      result := position - 1;
end;

function Animal.getCommand(): char;
begin
  result := self.command;
end;

var position: integer;
  skin, command: char;

  AnimalItem: Animal;
begin

  AnimalItem := Animal.create('*');
  while(AnimalItem.getCommand() <> 'q') do
  begin
    ClrScr();
    AnimalItem.drawFrame();
    AnimalItem.handleCommand();
  end;


  writeln('Программа завершена!');
  readln();
end.

Примечание: для лучшего понимания см. видео-разбор перехода от процедурного стиля к ООП: часть 1 разбора и часть 2 разбора.

Пример №2 - переписываем в ООП стиле более сложное управление сценой

Возьмем пример, где не нужно нажимать Enter для ввода команды и сцена обновляется вне зависимости от того посылает ли пользователь команды или нет:

uses Crt; //  импортираем модуль Crt
 
procedure drawFrame(skin: char; position: integer);
var i: integer;
begin
  for i:=1 to (position-1) do
    write(' ');
  writeln(skin);
end;
 
procedure handleCommand(var position: integer; var command: char);
begin
  command := ReadKey(); // вместо readln(command)
  if (command = '2') then
    position += 1
  else
    if (position > 1) then
      position -= 1;
end;
 
//  *
var position: integer;
  skin, command: char;
begin
  skin := '*'; // внешний вид
  command := '1'; // последняя введенная команда
  position := 1; // текущая позиция
  while(command <> 'q') do
  begin
    ClrScr(); // обновляем "кадр"
    drawFrame(skin, position); // рисуем очередной кадр
    Delay(500);
    {далее высчитываем новую позицию только если клавиша нажата,
      иначе просто отрисовываем кадр заново без изменений
    }
    if (keyPressed()) then
      handleCommand(position, command); // обрабатываем ввод
  end;
 
  writeln('Programma zavershena!');
 
  readln();
end. 

И перепишем его в ООП стиль по аналогии с предыдущим примером:

uses Crt; //  импортираем модуль Crt

type
  Animal = class
    public
      constructor create(skinValue: char);
      procedure drawFrame();
      procedure handleCommand();
      function getCommand(): char;
    protected
      skin: char; // внешний вид
      position: integer; // текущая позиция
      command: char; // последняя введенная команда
      function getNewPosition(commandValue: char): integer;
  end;

constructor Animal.create(skinValue: char);
begin
  self.skin := skinValue;
  self.command := ' ';  // значение по умолчанию
end;

procedure Animal.drawFrame();
var i: integer;
begin
  for i:=1 to (self.position-1) do
    write(' ');
  writeln(self.skin);
end;

procedure Animal.handleCommand();
begin
  self.command := ReadKey(); // вместо readln(command)
  self.position := self.getNewPosition(self.command);
end;

function Animal.getNewPosition(commandValue: char): integer;
begin
  if (commandValue = '2') then
    result := self.position + 1
  else
    if (position > 1) then
      result := self.position - 1;
end;

function Animal.getCommand(): char;
begin
  result := self.command;
end;

var
  AnimalItem: Animal;
begin
  AnimalItem := Animal.create('*');
  while(AnimalItem.getCommand() <> 'q') do
  begin
    ClrScr();
    AnimalItem.drawFrame();
    Delay(500);
    if (keyPressed()) then
       AnimalItem.handleCommand();
  end;

  writeln('Programma zavershena!');
  readln();
end.

Пример №3 - Вводим класс "Сцены"

Продолжим усовершенствовать наш, пример анимации с ООП из предыдущего примера, где он выглядел так:

uses Crt; //  импортираем модуль Crt

type
  Animal = class
    public
      constructor create(skinValue: char);
      procedure drawFrame();
      procedure handleCommand();
      function getCommand(): char;
    protected
      skin: char; // внешний вид
      position: integer; // текущая позиция
      command: char; // последняя введенная команда
      function getNewPosition(commandValue: char): integer;
  end;

constructor Animal.create(skinValue: char);
begin
  self.skin := skinValue;
  self.command := ' ';  // значение по умолчанию
end;

procedure Animal.drawFrame();
var i: integer;
begin
  for i:=1 to (self.position-1) do
    write(' ');
  writeln(self.skin);
end;

procedure Animal.handleCommand();
begin
  self.command := ReadKey(); // вместо readln(command)
  self.position := self.getNewPosition(self.command);
end;

function Animal.getNewPosition(commandValue: char): integer;
begin
  if (commandValue = '2') then
    result := self.position + 1
  else
    if (position > 1) then
      result := self.position - 1;
end;

function Animal.getCommand(): char;
begin
  result := self.command;
end;

var
  AnimalItem: Animal;
begin
  AnimalItem := Animal.create('*');
  while(AnimalItem.getCommand() <> 'q') do
  begin
    ClrScr();
    AnimalItem.drawFrame();
    Delay(500);
    if (keyPressed()) then
       AnimalItem.handleCommand();
  end;

  writeln('Programma zavershena!');
  readln();
end.
</pre>

Наша задача далее - ввести для анимации отдельный класс и инкапсулировать в нем реализацию цикла обработки событий, наличие такого класса позволит убрать из объекта живущего на сцене (Animal) взаимодействие с консолью:

uses Crt; //  импортираем модуль Crt

type
  Animal = class
    public
      constructor create(skinValue: char);
      procedure drawFrame();
      procedure handleCommand(commandValue: char);
      function getCommand(): char;
    protected
      skin: char; // внешний вид
      position: integer; // текущая позиция
      command: char; // последняя введенная команда
      function getNewPosition(commandValue: char): integer;
  end;

  Scene = class // Сцена Анимации
    public
      procedure handleCommand();
      procedure run(AnimalItem: Animal);
      function getCommand(): char;
    protected
      command: char; // последняя введенная команда
  end;

// -- Реализация Scene -------

procedure Scene.run(AnimalItem: Animal);
begin
  // обработки событий
  while(self.getCommand() <> 'q') do
  begin
    ClrScr();
    AnimalItem.drawFrame();
    Delay(500);
    if (keyPressed()) then
    begin
       self.handleCommand();
       AnimalItem.handleCommand(self.getCommand());
    end;
  end;

  writeln('Scena zavershena!');
end;

procedure Scene.handleCommand();
begin
  self.command := ReadKey(); // читаем символ из консоли
end;

function Scene.getCommand(): char;
begin
  result := self.command;
end;


// -- Реализация Animal -------
constructor Animal.create(skinValue: char);
begin
  self.skin := skinValue;
  self.command := ' ';  // значение по умолчанию
end;

procedure Animal.drawFrame();
var i: integer;
begin
  for i:=1 to (self.position-1) do
    write(' ');
  writeln(self.skin);
end;

procedure Animal.handleCommand(commandValue: char);
begin
  self.position := self.getNewPosition(commandValue);
end;

function Animal.getNewPosition(commandValue: char): integer;
begin
  if (commandValue = '2') then
    result := self.position + 1
  else
    if (position > 1) then
      result := self.position - 1;
end;

function Animal.getCommand(): char;
begin
  result := self.command;
end;
// ---------------
var
  AnimalItem: Animal;
  SceneItem: Scene;
begin
  AnimalItem := Animal.create('*');
  SceneItem :=  Scene.create(); // создаем сцену
  SceneItem.run(AnimalItem); // запускаем анимацию

  readln();
end.

-- архитектурно уже видно, что теперь сцена отвечат за напр. за время задержки между кадрами, и за цикл событий, также в run() можно передать более одного объекта и есть переписать способ их отрисовки (опять, же передав сцене, как минимум вывод "пустого пространства"), то на сцене можно будет управлять сразу несколькими объектами, чем мы и займемся далее.

Переносим управление "пространством" в класс сцены, вывод двух объектов

Перепишем последний пример кода, так:

uses Crt; //  импортираем модуль Crt

type
  Animal = class
    public
      constructor create(skinValue: char);
      procedure drawFrame();
      procedure handleCommand(commandValue: char);
      function getCommand(): char;
      function getPosition(): integer;
      function isCurrentPosition(position: integer): boolean;
    protected
      skin: char; // внешний вид
      position: integer; // текущая позиция
      command: char; // последняя введенная команда
      function getNewPosition(commandValue: char): integer;
  end;

  Scene = class // Сцена Анимации
    public
      procedure handleCommand();
      procedure run(AnimalItem, SecondAnimal: Animal);
      procedure drawSceneFrame(AnimalItem, SecondAnimal: Animal);
      function getCommand(): char;
    protected
      command: char; // последняя введенная команда
      function getMax(a, b: integer): integer;
  end;

// -- Реализация Scene -------

procedure Scene.run(AnimalItem, SecondAnimal: Animal);
begin
  // обработки событий
  while(self.getCommand() <> 'q') do
  begin
    ClrScr();
    // новый метод, для отрисовки кадра и всех объектов
    self.drawSceneFrame(AnimalItem, SecondAnimal);
    Delay(500);
    if (keyPressed()) then
    begin
       self.handleCommand();
       AnimalItem.handleCommand(self.getCommand());
       SecondAnimal.handleCommand(self.getCommand());
    end;
  end;

  writeln('Scena zavershena!');
end;

// Отрисовка кадра сцены
procedure Scene.drawSceneFrame(AnimalItem, SecondAnimal: Animal);
var maxPosition, i: integer;
begin
   maxPosition :=
     self.getMax(AnimalItem.getPosition(), SecondAnimal.getPosition());
   for i:=1 to maxPosition do
   begin
     if (AnimalItem.isCurrentPosition(i)) OR (SecondAnimal.isCurrentPosition(i)) then
     begin
       if (AnimalItem.isCurrentPosition(i)) then
         AnimalItem.drawFrame();
       if (SecondAnimal.isCurrentPosition(i)) then
         SecondAnimal.drawFrame();
     end else
       write(' ');
   end;
end;

function Scene.getMax(a, b: integer): integer;
begin
  if (a > b) then
    result := a
  else
    result := b;
end;

procedure Scene.handleCommand();
begin
  self.command := ReadKey(); // читаем символ из консоли
end;

function Scene.getCommand(): char;
begin
  result := self.command;
end;



// -- Реализация Animal -------
constructor Animal.create(skinValue: char);
begin
  self.skin := skinValue;
  self.command := ' ';  // значение по умолчанию
  self.position := 1;
end;

procedure Animal.drawFrame();
begin
  write(self.skin);
end;

// больше не работаем с консолью напрямую
procedure Animal.handleCommand(commandValue: char);
begin
  self.position := self.getNewPosition(commandValue);
end;

function Animal.getNewPosition(commandValue: char): integer;
begin
  if (commandValue = '2') then
    result := self.position + 1
  else
    if (position > 1) then
      result := self.position - 1;
end;

function Animal.getCommand(): char;
begin
  result := self.command;
end;

function Animal.getPosition(): integer;
begin
  result := self.position;
end;

function Animal.isCurrentPosition(position: integer): boolean;
begin
  result := (position = self.position);
end;
// ---------------
var
  AnimalItem, // первое животное
    Dog       // второе животное
    : Animal;
  SceneItem: Scene;
begin
  AnimalItem := Animal.create('*');
  Dog := Animal.create('@');
  SceneItem :=  Scene.create(); // создаем сцену
  SceneItem.run(AnimalItem, Dog); // запускаем анимацию

  readln();
end.

- в этом коде:

  • Класс сцены отвечает за заполнение пустого пространства (если в этой позиции нет объекта) или предлагает объекту нарисовать себя, если для очередной проверяемой координаты объект-животное отвечает. что "да, это мое положение в пространстве"
  • Объеты-"животные" больше не работают с консолью напрямую, символ команды считывает объекты Сцена
  • Сцена в своем метода отрисовки кадра вызывает методы отрисовки объетов

Подробности:

Пример №5 Разные клавиши для управления положением разных объектов

Можно переписать класс Animal так, чтобы положение каждого объекта этого класса управлялось своими клавишами:

uses Crt; //  импортираем модуль Crt

type
  Animal = class
    public
      constructor create(skinValue, moveLeftChar, moveRightChar: char);
      procedure drawFrame();
      procedure handleCommand(commandValue: char);
      function getCommand(): char;
      function getPosition(): integer;
      function isCurrentPosition(position: integer): boolean;
    protected
      moveLeftChar: char; // клавиша (символ) для движения влево
      moveRightChar: char; // клавиша (символ) для движения вправо
      skin: char; // внешний вид
      position: integer; // текущая позиция
      command: char; // последняя введенная команда
      function getNewPosition(commandValue: char): integer;
  end;

  Scene = class // Сцена Анимации
    public
      procedure handleCommand();
      procedure run(AnimalItem, SecondAnimal: Animal);
      procedure drawSceneFrame(AnimalItem, SecondAnimal: Animal);
      function getCommand(): char;
    protected
      command: char; // последняя введенная команда
      function getMax(a, b: integer): integer;
  end;

// -- Реализация Scene -------

procedure Scene.run(AnimalItem, SecondAnimal: Animal);
begin
  // обработки событий
  while(self.getCommand() <> 'q') do
  begin
    ClrScr();
    // новый метод, для отрисовки кадра и всех объектов
    self.drawSceneFrame(AnimalItem, SecondAnimal);
    writeln();
    Delay(50);
    if (keyPressed()) then
    begin
       self.handleCommand();
       AnimalItem.handleCommand(self.getCommand());
       SecondAnimal.handleCommand(self.getCommand());
    end;
  end;

  writeln('Scena zavershena!');
end;

// Отрисовка кадра сцены
procedure Scene.drawSceneFrame(AnimalItem, SecondAnimal: Animal);
var maxPosition, i: integer;
begin
   maxPosition :=
     self.getMax(AnimalItem.getPosition(), SecondAnimal.getPosition());
   for i:=1 to maxPosition do
   begin
     if (AnimalItem.isCurrentPosition(i)) OR (SecondAnimal.isCurrentPosition(i)) then
     begin
       if (AnimalItem.isCurrentPosition(i)) then
         AnimalItem.drawFrame();
       if (SecondAnimal.isCurrentPosition(i)) then
         SecondAnimal.drawFrame();
     end else
       write(' ');
   end;
end;

function Scene.getMax(a, b: integer): integer;
begin
  if (a > b) then
    result := a
  else
    result := b;
end;

procedure Scene.handleCommand();
begin
  self.command := ReadKey(); // читаем символ из консоли
end;

function Scene.getCommand(): char;
begin
  result := self.command;
end;



// -- Реализация Animal -------
constructor Animal.create(skinValue, moveLeftChar, moveRightChar: char);
begin
  self.skin := skinValue;
  self.moveLeftChar := moveLeftChar;
  self.moveRightChar := moveRightChar;
  self.command := ' ';  // значение по умолчанию
  self.position := 1;
end;

procedure Animal.drawFrame();
begin
  write(self.skin);
end;

// больше не работаем с консолью напрямую
procedure Animal.handleCommand(commandValue: char);
begin
  self.position := self.getNewPosition(commandValue);
end;

function Animal.getNewPosition(commandValue: char): integer;
begin
  result := self.position; // предыдущее значение, как значение по умолчанию
  if (commandValue = self.moveRightChar) then
    result := self.position + 1
  else
    if ((commandValue = self.moveLeftChar) AND (self.position > 1)) then
      result := self.position - 1;
end;

function Animal.getCommand(): char;
begin
  result := self.command;
end;

function Animal.getPosition(): integer;
begin
  result := self.position;
end;

function Animal.isCurrentPosition(position: integer): boolean;
begin
  result := (position = self.position);
end;
// ---------------
var
  AnimalItem, // первое животное
    Dog       // второе животное
    : Animal;
  SceneItem: Scene;
begin
  AnimalItem := Animal.create('*', '1', '2');
  Dog := Animal.create('@', '3', '4');
  SceneItem :=  Scene.create(); // создаем сцену
  SceneItem.run(AnimalItem, Dog); // запускаем анимацию

  readln();
end.

-- для лучшего понимания см. видео-разбор перехода к этому состоянию кода.

Key Words for FKN + antitotal forum (CS VSU):