В.М. Сапрыкин.
Несерьёзно о серьёзном.
Визуализация алгоритмов на уроках
программирования в средней школе.
Часть 1. Пока несерьёзная
Как
бы ни менялся мир, а человек продолжает играть. Младенец познаёт мир
посредством игр, подросток с их помощью пытается в нём утвердиться.
Неудивительно поэтому, что игровые технологии обучения продолжают удерживать
свои позиции, несмотря на все наши рассуждения о получении знаний, о
преимуществах человека образованного и о том, что нужно, всё-таки, быть
серьёзней.
С
появлением компьютеров само понятие игры вышло за рамки сугубо материального
мира, и прочно вторглось в виртуальную сферу, породив, по сути, целую отрасль. Стоит
ли удивляться, что, рассуждая о выборе такой, казалось бы, серьёзной и нужной профессии
как программист, многие школьники думают, прежде всего, о компьютерных играх, а
уже потом о каких-то «более серьёзных» вещах.
Какими
бы ни предполагались будущие достижения, для создания качественных программ
хорошему программисту нужна математика, нужна логика, нужен, в конце концов,
просто здравый смысл, хотя бы для того, чтобы написать работоспособную
алгоритмическую конструкцию. И вот тут-то, на этапе обдумывания алгоритма, чаще
всего и случается пробуксовка в сознании учащегося. «Слова все помню. Но как
расположить их в нужном порядке?»
Азы
школьного программирования обычно подразумевают обработку неких несложных
алгоритмов. Чаще математических. Однако сами учебные задачи нередко бывают
невыразимо скучными. «Что будет напечатано на экране в результате выполнения
программы?» «Какое значение переменной было выбрано, если программа вывела
следующий результат…?» И так далее. Разумеется, задачи вида «Что будет, если?» нужны
и важны. Но до осознания этой их важности ученику нужно как-то добраться. Просто
так, «в лоб», «потому что так надо», работать может не каждый. В учебной задаче,
всё же, должен быть виден практический смысл. Иначе, с какой целью её вообще
нужно решать? Если ученику скучно, он будет работать вполсилы, не более того. И
выяснение того, что же «будет напечатано на экране», отнюдь не означает, что
ученик научится писать программы. Считывать алгоритм – возможно, да. Но не
более. Составлять алгоритм решения скучной задачи – скучно. Во всяком случае,
школьнику. А если задача его заинтересовала, то он будет обдумывать её
тщательнее, стремиться вникнуть в детали. И в конце концов решит. Разве не об
этом мечтает каждый учитель?
Собственно
об этом я и хотел бы поговорить. Для того чтобы задача на программирование
стала интересней, в неё тоже можно внести игровой элемент. Один из самых
простых способов – визуализация алгоритма. Пусть начинающий программист собственными
глазами видит, как работает его программа. И результат будет выводиться не сухими
числами, а вполне понятной картинкой. Таким образом, мы решаем задачу «Как
сделать, чтобы?», с постоянным визуальным контролем этого самого «чтобы». То
есть, сочетаем полезное с приятным.
Один
из возможных вариантов – написание программ для реализации некой анимационной
модели. В качестве примера я покажу разбор простой задачи с условным названием
«пинг-понг», которую, в том или ином виде, время от времени предлагаю на уроках
своим ученикам. Требуется написать программу, которая имитирует на экране
компьютера летающий мяч, как в пинг-понге, от одной границы стола к другой. Мы
работаем в среде PascalABC.NET. Его
графическая библиотека, представленная подключаемым модулем GraphABC, содержит
замечательный набор готовых графических объектов, что позволяет ученику
сосредоточиться исключительно на отработке движения, не углубляясь в дебри
собственно отрисовки фигурки, каковое, собственно, и не входит в школьную
программу. То есть, мы начинаем описывать математическую модель полета шарика с
отскоком от препятствия. Ремарка: мы не изучаем тонкости упругой деформации,
являющиеся предметом исследований в физике. У нас отскок хоть и упругий, но эта
упругость, как и скорость движения, является константой, а потому специально не
рассматривается. У нас моделируется движение как таковое, и используется совсем
немного математики при расчёте направления отскока.
Итак,
алгоритм. Первое задание заключается в том, чтобы нарисовать движущийся шар.
Для простоты, пусть он движется слева направо, прямолинейно и равномерно,
только в одну сторону. Что для этого нужно? Ну, прежде всего, не забыть
подключить графический модуль (а такое иногда случается). И тогда уже вплотную
заняться математическим описанием движения.
В
этот момент отовсюду начинают выползать межпредметные связи. Нарисовать шарик
несложно, для этого в PascalABC.NET есть,
например, процедура Circle(x,y,r), где две первые
переменные указывают координаты центра окружности, а последняя – её радиус.
Задать произвольные координаты и радиус может практически каждый. И практически
каждый делает это при помощи готовых чисел. Но как только речь заходит об
описании движения шара, многие ученики начинают, что называется,
пробуксовывать. Во-первых, только сейчас по-настоящему вспоминают о
существовании координатной плоскости. Да ещё и состоящей из одного квадранта с
началом координат в левом верхнем углу, из чего следует, что все значения координат
X и Y в
видимой части экрана всегда положительны (см. рис. 1).
Во-вторых,
быстро выясняется, что движение шарика подразумевает периодическую смену
координат, а раз уж они меняются, то и готовые числа для подстановки в
процедуру не годятся. Нужны переменные. Задать набор переменных и их начальное
значение несложно. Но как обеспечить смену значений? Кто сразу угадал – тот
молодец, а всем остальным приходится подбрасывать мысль, что движение шарика по
горизонтали означает всего лишь поступательное приращение координаты Х и
прорисовывание нового шарика сразу после её смены. Что подразумевает
использование цикла.
Рис.
1 Координаты на экране.
Вообще,
подсказки целесообразно давать по частям, дозированно, чтобы ученик имел
возможность увидеть каждый этап создания готовой программы и понять, какая её часть
за что отвечает. Тогда, по завершении работы, он укладывает в сознании и модель
в целом, и взаимное влияние её частей. То есть, видит систему.
Поэтому
уже на данном этапе хорошо бы попробовать в действии то, что мы успели
придумать. Маленькая ремарка: кроме описанной выше процедуры отрисовки круга,
по крайней мере в NET-версии
Паскаля, существует ещё две: DrawCircle и FillCircle. Первая рисует окружность, то
есть, контур, а вторая – закрашенный круг без контура. Мы, с прицелом на
будущее, будем использовать именно вторую процедуру, из тех соображений, что нам
понадобится и окрасить круг, и отдельно задать цвет для «ракеток», и это должны
быть разные процедуры. Поэтому оптимальным будет именно такой выбор.
Итак,
цикл написан, переменная меняет своё значение. Шарик движется, ура, только его
изображение не стирается, а оставляет след и превращается на экране в сосиску.
Почему? Как раз потому, что мы его всякий раз рисуем на новом месте, но ни разу
не стираем. Стирать его можно по-разному, хотя бы закрашивая фоновым цветом круг
в той же координатной области. Но проще сделать так: нарисовать шар, после
некоторого времени экспозиции очистить изображение процедурой ClearWindow, и затем
уже изменять координаты для новой отрисовки.
Далее
идут детали. Для целочисленной переменной используем привычный тип Integer,
начальные координаты шарика задаём таким образом, чтобы его края не выходили за
границы окна вывода. Чтобы нарисовать окрашенный шарик используем процедуру FillCircle(x,y,r). Цвет
заливки шарика определяем процедурой SetBrushColor.
В
результате, первый запрограммированный алгоритм может быть примерно таким:
program
ping_pong;
uses
GraphABC;
var
x,y,r:integer;
begin
SetWindowSize(600,500);//Устанавливаем
размер окна вывода
x:=50; y:=50; r:=50;
SetBrushColor(clRed);//устанавливаем
цвет шарика
for
var k:=1 to 250 do
begin
FillCircle(x,y,r);//отрисовка шарика
Sleep(2);
//задержка его «экспозиции» на 2мс.
ClearWindow;
//очистка изображения
x:=x+2;
//сдвиг центра (да, собственно, и всего) шарика вправо
end;
end.
Приращение координаты Х именно на два пикселя выбрано для
того, чтобы шарик не слишком медленно перемещался, иначе смотреть на него
просто скучно. В приведённом примере шарик смещается по экрану на 500 точек
слева направо (2 pix*250 шагов цикла). Учитывая его радиус в 50 точек, несложно
подсчитать, что визуально он двигается ровно от границы до границы окна (600pix),
соприкасаясь с ней, а также с верхней кромкой, краем окружности. Чтобы опустить
его пониже, нужно увеличить Y.
Итак, есть первая победа: шарик пролетел по экрану в одну сторону.
Следовательно, готов «отскакивать». Что нужно сделать, чтобы он вернулся
обратно, от правой границы к левой? Тут уже легче. Базовая математическая
модель освоена, в работу включается логика. Ученики осознают, что нужен второй
цикл, сразу после первого, и в нём координата Х должна не увеличиваться,
а уменьшаться. А как сделать, чтобы шарик несколько раз отскакивал от стенок
слева и справа? Возможно, не все сразу предложат идею, и тогда можно немного помочь,
предложив использовать внешний цикл, внутри которого будут поочерёдно
выполняться два основных цикла.
Для вящей визуализации можно предложить ученикам изменить
программу так, чтобы шар при каждом отскоке от «стенки» менял свой цвет.
Например, пусть он слева направо летит красным, а, отскочив от стенки, летит
влево уже синим. Здесь проблем быть не должно, и если ученики не догадываются
сразу, что нужно сделать, я не спешу подсказывать. Требуется минимум
рассуждений, и отнимать у них возможность испытать «озарение» я не намерен.
Примерный вид программы с перекрашиванием шарика в момент отскока может быть
таким (заголовки вложенных циклов для наглядности набраны цветом,
соответствующим цвету шара):
program
ping_pong;
uses
GraphABC;
var
x,y,r:integer;
begin
SetWindowSize(600,500);
x:=50; y:=50; r:=50;
for
var n:=1 to 5 do
begin
SetBrushColor(clRed);
for
var k:=1 to 250 do //красный цикл (полёт красного шарика вправо)
begin
FillCircle(x,y,r);
Sleep(2);
ClearWindow;
x:=x+2;
end;
SetBrushColor(clBlue);
for
var k:=1 to 250 do //синий цикл, полёт влево
begin
FillCircle(x,y,r);
Sleep(2);
ClearWindow;
x:=x-2;
end;
end;
end.
В данной модели наблюдается мерцание изображения шарика, но мы его
игнорируем как несущественное, главное – летит. Способ избавиться от этого
явления будет рассмотрен далее.
Часть 2. Играем серьёзней
На этом «базовая» часть моделирования отскока шарика завершена.
Более продвинутым ученикам можно предлагать к реализации более сложную модель. Цель
моделирования проста: получить более правдоподобное изображение полёта и
отскакивания шарика.
Основные изменения в модели таковы. Во-первых, пусть шарик
отскакивает не от стенок окна, а всё же от ракеток, которые и нужно
прорисовать. Условно, в виде полосок, но сделать. Во-вторых, настоящий шарик
ведь практически никогда не отскакивает по нормали к поверхности. Напротив,
игроки стараются отбивать его немного вбок. Значит, для вящей красоты
композиции хорошо бы продумать математическую модель, описывающую отскок шарика
от ракетки с произвольным (случайным) углом отклонения от нормали. Главное,
чтобы он не вылетел с игрового стола. И в-третьих, принимающая сторона должна
этот движущийся по произвольной траектории шарик отбить. Для чего требуется
аккуратно и плавно подвести ракетку в место предполагаемого удара шарика. Для
простоты принимаем траекторию движения шарика линейной. Таким образом, нам
нужно визуально реализовать лишь отскок шарика под случайным углом, прямолинейное
его движение и последующее отбивание.
Сложно? О да, конечно, если пытаться решить задачу «в лоб»,
выдумывая угол. На самом деле наша задача – нарисовать движение шарика
под случайным углом, а вовсе не вычислять величину угла. Зачем нам, в сущности,
этот самый случайный угол? Вспомним: когда шарик летел горизонтально, мы
описывали его движение положительным или отрицательным приращением координаты Х.
Координата Y не менялась, а если бы она изменилась, тогда – что?
Вот, первый успех достигнут! Нужно всего лишь обеспечить
монотонное приращение обеих координат, X и Y. Тогда шарик будет смещаться и по
горизонтали, и по вертикали, то есть, лететь наискось. Но линейное смещение по
оси X происходит просто (х:=х+2),
а что делать с осью Y? Насколько смещать центр шара, и в каждом
ли шаге? Если одинаково с Х, то отскок будет происходить под углом 45°.
А как реализовать другие углы?
Варианты решения могут быть разными. Один из достаточно простых
способов выглядит так. Перед началом каждого цикла запускаем генератор
случайных чисел, который и используем для установки конечной величины смещения
по Y. А для того, чтобы шарик прилетал именно
в это место, для каждого шага цикла просчитываем величину приращения ординаты
по формуле: разность начальной и конечной ординаты, делённая на количество
шагов по оси Х и умноженная на номер шага. Количество шагов – константа,
равная полному количеству шагов цикла. А номер шага – это, конечно же, текущее
значение счётчика цикла (k). И вот это приращение (dY) мы и
используем в каждом шаге: (x:=x+2;
y:=y+dy).
Движение «ракеток» тайно координируем с этим же параметром, тогда
они будут прилетать на нужную высоту ровно в тот момент, когда туда прилетит
шарик. Для упрощения расчётов можно принять, что обе ракетки движутся синхронно
(как будто одна постоянно наблюдает за другой и подстраивается под неё). Тогда
описание движения обеих ракеток будет идентичным описанию движения шарика, но
только по координате Y (относительно оси X они
не движутся).
В данной модели вообще следует обратить пристальное внимание на
параметрическую, то есть, зависимую установку значений переменных. В противном
случае движение элементов модели просто рассинхронизируется, и выглядеть будет
странно.
Предлагаемый ниже вариант программы отличается от приведённого
выше тем, что, во-первых, показывает «ракетки», от которых отскакивает шарик,
во-вторых, имитирует случайный угол отскока шарика, и, в-третьих, описывает
перемещение ракеток так, чтобы отбить подлетающий шарик. Причём, они настолько
точно «угадывают» траекторию шарика, что подставляют ему точно свою середину.
Все это достигается взаимной связью переменных, описывающих положение ракеток и
шарика в графическом окне.
Решение выглядит так. Отрисовка подвижных объектов производится
при помощи процедур Line(x1,y1,x2,y2)(конечно, для левой и правой ракеток по отдельности) и FillCircle(x,y,r).
Координаты, описывающие движение, всегда относятся к центру фигуры, - для
простоты расчётов. С кругом это очевидно, он в любом случае строится от центра,
а для вертикальных линий координаты верхней и нижней точки рассчитываются от
средней, простым вычитанием и прибавлением константы (в нашем случае это 50
пикселей вверх и вниз).
Набор переменных: x,xl,xr,yl,yr,r,rnd.
Однобуквенные переменные относятся к отрисовке шарика. Отдельная переменная Y для него не задействуется, поскольку ордината центра шарика всё равно
всегда совпадает с ординатой центра одной из ракеток, так что плодить лишние
переменные не имеет смысла. В момент отскока шарика ордината
его конечной позиции (и центра противоположной ракетки) уже известна, и к ней пошагово
приближается не только шарик, но и обе ракетки.
Двухбуквенные координаты приняты для обозначения границ левой и
правой ракеток. Трехбуквенная переменная – то самое случайное число, которое и
подсказывает шарику, в каком направлении ему следует лететь. Здесь можно
поиграть с е величиной числа.
Ордината центра шарика в каждой точке цикла в процессе движения (здесь:
слева направо) рассчитывается так: yr:=yl+round((100*rnd-yl)/250*k).
При выводе формулы следует обратить особое внимание учеников на округление
результата. Необходимо, чтобы они закрепили в сознании тот факт, что координаты
любой точки, в сущности, представляют собой количество пикселей, на которые она
отстоит от начала координат. Деление, используемое в формуле, автоматически
приводит к возникновению вещественного числа (т.е. возможно получение дробного
значения), а количество пикселей дробным быть не может. Именно эта ордината
использована для смещения по вертикали и шарика, и ракетки, вот почему шарику
не нужна собственная переменная Y.
Готовая программа с учётом вышесказанного выглядит следующим
образом:
program
ping_pong;
uses
GraphABC;
var
x,xl,xr,yl,yr,r,rnd:integer;
begin
SetWindowSize(620,500);
xl:=10; yl:=100;
xr:=610; yr:=100;
r:=50;
SetPenColor
(clDarkBlue); //задаём цвет ракеток
SetPenWidth
(5);
Line(xl,yl-50,xl,yl+50);
Line(xr,yr-50,xr,yr+50);
x:=xl+r;
SetBrushColor(clBlue);
FillCircle(x,yl,r);
//рисуем шарик на стартовой позиции, здесь он синий
Sleep(500); //и
даём ему полсекунды повисеть, затем он улетает вправо
LockDrawing; //отключение
непосредственного рисования на экране
for
var n:=1 to 6 do begin
rnd:=random(1,4);
for
var k:=1 to 250 do begin
ClearWindow;
//очистка содержимого окна
yr:=yl+round((100*rnd-yl)/250*k);
Line(xr,yr-50,xr,yr+50);
Line(xl,yr-50,xl,yr+50);
SetBrushColor(clRed);
//улетающий шарик уже красный, а не синий
FillCircle(x,yr,r);
Redraw;
//перерисовка графического окна целиком
//Sleep(1);
x:=x+2;
end;
rnd:=random(1,3);
for
var k:=1 to 250 do begin
ClearWindow;
yl:=yr+round((100*rnd-yr)/250*k);
Line(xl,yl-50,xl,yl+50);
Line(xr,yl-50,xr,yl+50);
SetBrushColor(clGreen);
FillCircle(x,yl,r);
Redraw;
//Sleep(1);
x:=x-2;
end;
end;
end.
Разумеется,
ее можно оптимизировать, в частности, уменьшив количество переменных. Но в
данном случае эту программу предполагается использовать как заготовку для
следующей её версии, несколько более сложной математически. Здесь, для
упрощения расчётов, обе ракетки движутся синхронно, используя одну и ту же
ординату. При этом, кстати, одна ракетка отбивает шарик чуть более
непредсказуемо, чем другая.
Продвинутым
ученикам можно предложить дописать программу таким образом, чтобы в момент
отскока шарика начинала двигаться только принимающая ракетка, а отбившая удар
оставалась бы неподвижной до момента следующего отскока, и только тогда уже начала
бы движение, чтобы в свою очередь отбить шарик. Математические расчёты здесь не
столь сложны, однако чтобы к ним прийти, нужно немного поразмыслить. Это мы
оставим за рамками данной статьи, чтобы не сбивать дополнительными раздумьями
игровой настрой.
Команды
задержки выполнения – Sleep – в
обоих циклах отключены за ненадобностью, поскольку расчёт всех операций сам по
себе даёт нужную задержку, и шарик вовсе не летает со скоростью пули. Но для
наглядности я оставил их (в «закомментированном» виде) в том месте, где им, при
необходимости, следует находиться.
Напоследок
несколько слов о мерцании. Если заглянуть в справочный файл, входящий в
дистрибутивный пакет PascalABC.NET, то в нём
можно найти способ устранения мерцания при перерисовке изображения. Основная
идея состоит в отключении рисования на самом экране, оставив прорисовку
виртуальную, только во внеэкранном буфере. Для этого нужно поместить в текст
программы команду LockDrawing. После этого
всякий раз (в каждом шаге цикла) следует формировать новый кадр изображения и
выводить его целиком на экран (командой Redraw). При вызове Redraw перерисовывается
все графическое окно, поэтому скорость анимации ограничена скоростью вывода
внеэкранного буфера на экран. Но мерцание, то есть, именно кратковременное
пропадание из видимости летящего шарика при этом действительно исчезает.
А
ещё можно предложить ученикам отключить процедуру ClearWindow в одном
из циклов и объяснить суть произошедших на экране изменений.
Напоследок
пожелаю всем успехов в обучении. Играйте серьёзно!
В.М. Сапрыкин
МБОУ Лицей №7 г. Химки
Оставьте свой комментарий
Авторизуйтесь, чтобы задавать вопросы.