Перейти к основному содержимому

SmallPong на Morphic — практикум

Разработчику Начальный уровень

О чём эта статья

Пошаговый практикум — вы соберёте графическую игру "пинг-понг" в Pharo на Morphic, по шагам в Class Browser, с разбором каждого класса и метода. Логика матча живёт в PongGame, интерфейс — в морфах; связь между ними повторяет идеи MVC из ООП в Smalltalk и десктопной архитектуры.

База: Первая программа, ООП-модель, Morphic. Соседний практикум без игрового цикла — крестики-нолики. Эталон для сверки — полная ревизия в конце статьи или ваш .st-файл после прохождения этапов.


Требования


Коротко об оригинале Pong

Pong (Atari, 1972) — одна из первых коммерчески успешных аркад. Два игрока отбивают мяч ракетками по горизонтали; проигрывает тот, кто пропустил мяч за свою линию. Вся "сложность" — в тайминге и угле отскока. Учебная версия повторяет петлю "ввод → движение → столкновение → счёт", которую в Python — Ping Pong масштабируют до меню, ИИ и конечного автомата.


Словарь

ТерминВ SmallPong
ImageЖивой образ Pharo — код и объекты в памяти; сохраняется через File → Save
Морф (Morph)Объект UI, который рисует себя и принимает события — Morphic
МодельPongGame — координаты, скорости, счёт; не знает о морфах
SteppingПериодический вызов step у морфа (~60 кадров/с при stepTime 16)
ПротоколКатегория методов в Browser (game, accessing, display)
FileInЗагрузка .st-файла с объявлениями классов в image

Управление

КлавишаДействие
W / SЛевая ракетка
/ Правая ракетка
ПробелПауза / продолжить
Кнопка "Новая игра"Сброс матча

Кликните по окну игры для фокуса клавиатуры.


Что получится

ЭлементПоведение
Поле 600×400Тёмно-зелёный фон, серая линия по центру
Две ракеткиБелые прямоугольники, W/S и стрелки
МячЖёлтый квадрат 12×12, отскок от стен и ракеток
СчётСтрока вида 3 : 7, победа при 11 очках
ПаузаПробел; кнопка "Новая игра" сбрасывает матч

Три класса в категории SmallPong:

КлассРоль
PongGameМодель — физика, счёт, пауза, победа
PongPlayfieldMorphДекор поля — центральная линия
PongGameMorphОкно — морфы, stepping, клавиатура
Чем отличается от Python/Pygame

В Pygame вы сами крутите цикл while running, зовёте pygame.event.get() и рисуете на screen. В Pharo морф подписывается на startStepping, а Morphic периодически шлёт ему step. Модель (PongGame) не знает о морфах — только числа и флаги. Подробный трек с FSM и ИИ — Python — Ping Pong.

Оценка времени — 2–4 часа при прохождении всех этапов в Class Browser.

Карта этапов

ЭтапФокусРезультат
0Подготовка PharoКатегория SmallPong, Browser открыт
1PongGame — каркасКонстанты поля, initialize, resetGame
2Доступ и вводГеттеры, флаги leftUp / rightDown
3step — стеныМяч летит и отскакивает от верха/низа
4Ракетки и голОтскок от ракеток, счёт, gameOver
5PongPlayfieldMorphЦентральная линия на поле
6PongGameMorph — UIПоле, ракетки, мяч, счёт, кнопка
7SteppingЖивой цикл ~60 FPS
8Клавиатура и запускPongGameMorph open
9FileInЗагрузка готового .st

Как проходить практикум

  1. Создайте категорию SmallPong в Class Browser.
  2. Идите этапы 0–9 по порядку — после каждого метода жмите Accept (Ctrl+S).
  3. Запускайте PongGameMorph open в Playground; кликните по окну для фокуса клавиатуры.
  4. Отмечайте Самопроверку; при расхождении — полная ревизия.

Архитектура

Три класса повторяют схему model + view + controller — как в SmallShooter и десктопных приложениях:

PongGameMorph ← окно, клавиатура, startStepping
├── StringMorph ← счёт
├── SimpleButtonMorph ← "Новая игра"
└── PongPlayfieldMorph ← drawOn — линия, ракетки, мяч
└── game: PongGame ← координаты и физика без Morphic

Определение. Stepping — встроенный игровой цикл Morphic: морф с startStepping получает step каждые stepTime миллисекунд (~16 ms → 60 FPS). В Pygame Pong тот же цикл вы пишете через while running и clock.tick().

КлассРоль
PongGameМодель — мяч, ракетки, счёт, пауза
PongPlayfieldMorphРисование поля и объектов
PongGameMorphОкно, stepping, клавиатура

Этап 0 — подготовка

Цель — рабочая среда Pharo и категория для классов.

Что сделать

  1. Установите Pharo 10+первая программа.
  2. Откройте Class Browser (Nautilus / System Browser).
  3. Создайте категорию классов SmallPong — правая панель → +Add category.
  4. Убедитесь, что Playground выполняет:
Transcript show: 'ok'; cr

(нужно открытое окно Transcript).

Теория

  • Категория классов — пространство имён в Browser, не пакет на диске. Все три класса SmallPong лягут в SmallPong.
  • Accept (Ctrl+S) — единственный способ "вкомпилировать" метод в image; без него Playground увидит старую версию или MessageNotUnderstood (FAQ раздела).

Самопроверка

  • Pharo запускается, Browser показывает категории.
  • После Accept тестовый метод виден в списке протоколов класса.

Этап 1 — класс PongGame, инициализация

Цель — модель игры без UI: размеры поля, сброс состояния, подача мяча.

Сначала логика, потом морфы — тот же порядок, что в практикуме "Крестики-нолики".

1.1. Объявление класса

В Browser: +Class. Шаблон:

Object subclass: #PongGame
instanceVariableNames: 'fieldWidth fieldHeight paddleWidth paddleHeight ballSize paddleSpeed ballSpeed winningScore ballX ballY ballVX ballVY leftPaddleY rightPaddleY leftScore rightScore leftUp leftDown rightUp rightDown paused gameOver winner'
classVariableNames: ''
poolDictionaries: ''
category: 'SmallPong'!

Accept.

Инстанс-переменные хранят всё состояние матча — координаты, скорости, флаги клавиш, счёт. UI читает их только через методы протокола accessing (этап 2).

1.2. Инициализация

Протокол initialization:

initialize
super initialize.
fieldWidth := 600.
fieldHeight := 400.
paddleWidth := 12.
paddleHeight := 80.
ballSize := 12.
paddleSpeed := 8.
ballSpeed := 5.
winningScore := 11.
paused := false.
self resetGame

Разбор:

  • super initialize — обязательный вызов родителя Object (инкапсуляция).
  • Числа после :=константы поля в пикселях логической модели (не окна Morphic).
  • self resetGame — один вход для "новый матч"; initialize и кнопка "Новая игра" вызывают его же.

1.3. Сброс игры и подача

Протокол game:

resetGame
leftScore := 0.
rightScore := 0.
gameOver := false.
winner := nil.
paused := false.
leftUp := false.
leftDown := false.
rightUp := false.
rightDown := false.
self resetPaddles.
self resetBall

resetPaddles
leftPaddleY := fieldHeight // 2 - (paddleHeight // 2).
rightPaddleY := leftPaddleY

resetBall
| direction |
ballX := fieldWidth // 2 - (ballSize // 2).
ballY := fieldHeight // 2 - (ballSize // 2).
direction := #( -1 1 ) atRandom.
ballVX := ballSpeed * direction.
ballVY := ballSpeed * (#( -1 1 ) atRandom)

Разбор:

  • // — целочисленное деление; центр мяча совпадает с центром поля.
  • #( -1 1 ) — литерал массива; atRandom выбирает направление влево или вправо по X и вверх/вниз по Y.
  • resetBall после гола — подача из центра с новой случайной траекторией.
  • winner := nil — пока никто не набрал 11; позже туда попадёт #Left или #Right (символ — именованная константа в Smalltalk).

1.4. Проверка в Playground

| g |
g := PongGame new.
g leftScore.
g rightScore.
g ballPosition

Ожидается 0, 0 и точка Point около 294@194 (центр поля минус половина мяча).


Этап 2 — доступ к состоянию и ввод

Цель — UI сможет читать координаты и передавать нажатия клавиш, не лезя в инстанс-переменные напрямую.

2.1. Протокол accessing

ballPosition
^ ballX @ ballY

fieldExtent
^ fieldWidth @ fieldHeight

paddleExtent
^ paddleWidth @ paddleHeight

leftPaddleOrigin
^ 10 @ leftPaddleY

rightPaddleOrigin
^ (fieldWidth - 10 - paddleWidth) @ rightPaddleY

leftScore
^ leftScore

rightScore
^ rightScore

paused
^ paused

gameOver
^ gameOver

winner
^ winner

Разбор:

  • @ — создание объекта Point (x@y); так Morphic задаёт позиции.
  • leftPaddleOrigin — отступ 10 px от левого края поля; правая ракетка симметрично у правого края.
  • Координаты модели — от левого верхнего угла поля (0@0), ось Y растёт вниз — как на экране (справочник Morphic).
  • Методы leftScore / rightScoreгеттеры с тем же именем, что и переменная; внутри ^ leftScore возвращает инстанс-переменную.

2.2. Протокол input

leftUp: aBoolean
leftUp := aBoolean

leftDown: aBoolean
leftDown := aBoolean

rightUp: aBoolean
rightUp := aBoolean

rightDown: aBoolean
rightDown := aBoolean

togglePause
paused := paused not

Разбор:

  • Суффикс : в имени — ключевое сообщение с одним аргументом (синтаксис).
  • Булевы флаги вместо "какая клавиша нажата" в модели — морф собирает клавиши в Set и раз в кадр вызывает syncInput (этап 8).
  • paused not — унарное сообщение объекту false/true.

2.3. Текст для HUD

Протокол game (дополните):

scoreText
^ leftScore asString, ' : ', rightScore asString

statusText
gameOver ifTrue: [
winner = #Left
ifTrue: [ ^ 'Победил левый игрок!' ]
ifFalse: [ ^ 'Победил правый игрок!' ] ].
paused ifTrue: [ ^ 'Пауза (пробел — продолжить)' ].
^ 'W/S — левый, ↑/↓ — правый, пробел — пауза'

Разбор:

  • , у String — конкатенация; asString у SmallInteger даёт '0', '11' и т.д.
  • Вложенные ifTrue: / ifFalse: — ранний возврат строки статуса; иначе подсказка по управлению.
  • winner = #Left — сравнение с символом; #Right обрабатывается веткой ifFalse:.

2.4. Проверка

PongGame new scoreText

'0 : 0'.


Этап 3 — метод step, движение и стены

Цель — один тик симуляции — ракетки по флагам, мяч движется, отскок от верхней и нижней границы.

3.1. Код

Протокол game:

step
gameOver ifTrue: [ ^ self ].
paused ifTrue: [ ^ self ].
leftUp ifTrue: [
leftPaddleY := (leftPaddleY - paddleSpeed) max: 0 ].
leftDown ifTrue: [
leftPaddleY := (leftPaddleY + paddleSpeed) min: fieldHeight - paddleHeight ].
rightUp ifTrue: [
rightPaddleY := (rightPaddleY - paddleSpeed) max: 0 ].
rightDown ifTrue: [
rightPaddleY := (rightPaddleY + paddleSpeed) min: fieldHeight - paddleHeight ].
ballX := ballX + ballVX.
ballY := ballY + ballVY.
(ballY <= 0) ifTrue: [
ballY := 0.
ballVY := ballVY abs ].
(ballY + ballSize >= fieldHeight) ifTrue: [
ballY := fieldHeight - ballSize.
ballVY := ballVY abs negated ]

3.2. Разбор

  • ^ self при gameOver и pausedранний выход; остальная физика не выполняется.
  • max: 0 и min: fieldHeight - paddleHeight — ракетка не выходит за поле по вертикали.
  • ballVX / ballVY — скорость в пикселях за тик (не за секунду); при stepTime 16 мс это ~312 px/с по каждой оси при ballSpeed = 5.
  • Отскок от "стены" — принудительно ставим ballY на границу и разворачиваем ballVY через abs / negated.

3.3. Порядок в кадре (важно на этапе 4)

На каждом тике соблюдайте цепочку:

  1. Движение ракеток по флагам.
  2. Движение мяча.
  3. Столкновения со стенами.
  4. (этап 4) Столкновения с ракетками и гол.

Если поменять местами "мяч" и "ракетки", мяч на один кадр окажется внутри ракетки — типичная причина "залипания" (частые ошибки).

3.4. Проверка

| g |
g := PongGame new.
20 timesRepeat: [ g step ].
g ballPosition

Координата Y меняется; при ударе о верх/низ знак ballVY меняется (смотрите в Inspector).


Этап 4 — ракетки, гол и победа

Цель — столкновения с ракетками (с углом отскока), очки за пропущенный мяч, победа до 11.

4.1. Полный метод step

Замените метод step на версию со столкновениями (локальные переменные в начале метода):

step
| leftRect rightRect ballRect relativeHit |
gameOver ifTrue: [ ^ self ].
paused ifTrue: [ ^ self ].
leftUp ifTrue: [
leftPaddleY := (leftPaddleY - paddleSpeed) max: 0 ].
leftDown ifTrue: [
leftPaddleY := (leftPaddleY + paddleSpeed) min: fieldHeight - paddleHeight ].
rightUp ifTrue: [
rightPaddleY := (rightPaddleY - paddleSpeed) max: 0 ].
rightDown ifTrue: [
rightPaddleY := (rightPaddleY + paddleSpeed) min: fieldHeight - paddleHeight ].
ballX := ballX + ballVX.
ballY := ballY + ballVY.
(ballY <= 0) ifTrue: [
ballY := 0.
ballVY := ballVY abs ].
(ballY + ballSize >= fieldHeight) ifTrue: [
ballY := fieldHeight - ballSize.
ballVY := ballVY abs negated ].
leftRect := self leftPaddleOrigin extent: self paddleExtent.
rightRect := self rightPaddleOrigin extent: self paddleExtent.
ballRect := ballX @ ballY extent: ballSize @ ballSize.
(ballVX < 0 and: [ leftRect intersects: ballRect ]) ifTrue: [
ballX := leftRect right + 1.
relativeHit := ((ballY + (ballSize / 2)) - leftPaddleY) / paddleHeight - 0.5.
ballVY := relativeHit * ballSpeed * 1.6.
ballVX := ballSpeed abs ].
(ballVX > 0 and: [ rightRect intersects: ballRect ]) ifTrue: [
ballX := rightRect left - ballSize - 1.
relativeHit := ((ballY + (ballSize / 2)) - rightPaddleY) / paddleHeight - 0.5.
ballVY := relativeHit * ballSpeed * 1.6.
ballVX := ballSpeed abs negated ].
(ballX + ballSize < 0) ifTrue: [
rightScore := rightScore + 1.
self checkWinFor: #Right.
self resetBall ].
(ballX > fieldWidth) ifTrue: [
leftScore := leftScore + 1.
self checkWinFor: #Left.
self resetBall ]

4.2. Проверка победы

checkWinFor: aSide
(leftScore >= winningScore) ifTrue: [
gameOver := true.
winner := #Left.
^ self ].
(rightScore >= winningScore) ifTrue: [
gameOver := true.
winner := #Right ]

Параметр aSide здесь не используется — метод вызывается для побочного эффекта; при желании добавьте логирование в Transcript.

4.3. Разбор физики

  • leftRect := origin extent: — прямоугольник ракетки в координатах модели (коллекции и числа).
  • intersects: — пересечение Rectangle мяча и ракетки.
  • Условие ballVX < 0 — мяч летит влево, имеет смысл проверять только левую ракетку (и наоборот).
  • relativeHit от −0.5 (верх ракетки) до 0.5 (низ) задаёт новую ballVYудар по краю даёт круче угол; тот же приём в Python — Ping Pong, этап 9.
  • Гол — ballX + ballSize < 0 (мяч ушёл влево → очко правому); затем resetBall, матч продолжается, если gameOver ещё false.

4.4. Проверка

| g |
g := PongGame new.
g leftDown: true.
100 timesRepeat: [ g step ].
g leftScore.
g rightScore

При пропущенных мячах счёт растёт; после 11 очков g gameOvertrue.


Этап 5 — PongPlayfieldMorph

Цель — морф поля с центральной линией; кастомная отрисовка поверх заливки.

5.1. Объявление класса

Morph subclass: #PongPlayfieldMorph
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'SmallPong'!

5.2. Отрисовка

Протокол display:

drawOn: aCanvas
| midX top bottom |
super drawOn: aCanvas.
midX := self bounds center x.
top := self bounds top + 8.
bottom := self bounds bottom - 8.
aCanvas
lineFrom: midX @ top
to: midX @ bottom
width: 2
color: (Color gray alpha: 0.35)

Разбор:

  • drawOn: — хук Morphic; вызывается при перерисовке морфа.
  • super drawOn: — заливает прямоугольник цветом self color.
  • self bounds — прямоугольник морфа в координатах родителя; линия чуть короче поля (отступ 8 px).
  • Color gray alpha: 0.35 — полупрозрачная серая линия, как сетка корта.

5.3. Проверка

PongPlayfieldMorph new
color: (Color r: 0.05 g: 0.12 b: 0.05);
extent: 600@400;
openInHand

Появится зелёное поле с серой линией по центру.


Этап 6 — PongGameMorph, сборка UI

Цель — окно с полем, ракетками, мячом, счётом, статусом и кнопкой "Новая игра".

По структуре похоже на TTTBoardMorph — родительский морф собирает детей и связывает их с моделью.

6.1. Объявление класса

Morph subclass: #PongGameMorph
instanceVariableNames: 'game playfieldOrigin playfieldMorph leftPaddleMorph rightPaddleMorph ballMorph scoreMorph statusMorph newGameButton pressedKeys'
classVariableNames: ''
poolDictionaries: ''
category: 'SmallPong'!

6.2. initialize и buildUI

Протокол initialization:

initialize
super initialize.
self color: Color black.
game := PongGame new.
pressedKeys := Set new.
playfieldOrigin := 10 @ 50.
self buildUI

buildUI
| fieldExtent |
fieldExtent := game fieldExtent.
playfieldMorph := PongPlayfieldMorph new.
playfieldMorph color: (Color r: 0.05 g: 0.12 b: 0.05).
playfieldMorph extent: fieldExtent.
playfieldMorph position: playfieldOrigin.
self addMorph: playfieldMorph.

leftPaddleMorph := Morph new.
leftPaddleMorph color: Color white.
leftPaddleMorph extent: game paddleExtent.
self addMorph: leftPaddleMorph.

rightPaddleMorph := Morph new.
rightPaddleMorph color: Color white.
rightPaddleMorph extent: game paddleExtent.
self addMorph: rightPaddleMorph.

ballMorph := Morph new.
ballMorph color: Color yellow.
ballMorph extent: game ballSize @ game ballSize.
self addMorph: ballMorph.

self extent: (fieldExtent x + 20) @ (fieldExtent y + 60).

scoreMorph := StringMorph contents: game scoreText font: (TextStyle default fontOfSize: 24).
scoreMorph color: Color white.
self addMorph: scoreMorph.
scoreMorph position: (fieldExtent x // 2 - 10) @ 10.

statusMorph := StringMorph contents: game statusText font: (TextStyle default fontOfSize: 12).
statusMorph color: Color lightGray.
self addMorph: statusMorph.
statusMorph position: 10 @ 28.

newGameButton := SimpleButtonMorph new.
newGameButton label: 'Новая игра'; font: (TextStyle default fontOfSize: 14).
newGameButton target: self; actionSelector: #newGame.
self addMorph: newGameButton.
newGameButton position: (fieldExtent x - 90) @ 8.
self refreshDisplay

Разбор:

  • playfieldOrigin := 10 @ 50 — поле сдвинуто вниз; верхние ~50 px — чёрный HUD (счёт, статус, кнопка).
  • addMorph: — дочерний морф рисуется поверх родителя; порядок добавления влияет на Z-order.
  • SimpleButtonMorph + target:actionSelector: — кнопка шлёт #newGame получателю self (события Morphic).
  • StringMorph — только отображение текста; содержимое обновляем в refreshDisplay.

6.3. Синхронизация модели и морфов

Протокол display:

refreshDisplay
| origin |
origin := playfieldOrigin.
leftPaddleMorph position: origin + game leftPaddleOrigin.
rightPaddleMorph position: origin + game rightPaddleOrigin.
ballMorph position: origin + game ballPosition.
scoreMorph contents: game scoreText.
statusMorph contents: game statusText

Протокол actions:

newGame
game resetGame.
pressedKeys removeAll.
self refreshDisplay

Разбор:

  • origin + game leftPaddleOriginперевод координат из модели (0..600) в координаты окна (сдвиг на HUD).
  • Модель не знает о playfieldOrigin — смещение только во view; чистое разделение MVC.
  • pressedKeys removeAll — после сброса не остаётся "залипших" клавиш.

6.4. Проверка

PongGameMorph new openInHand

Статичная картинка — поле, ракетки по центру, счёт 0 : 0. Мяч пока не двигается (нет stepping).


Этап 7 — игровой цикл stepping

Цель — мяч и ракетки обновляются автоматически, без ручного step в Playground.

7.1. Код

В конец initialize добавьте:

self startStepping

Протокол stepping:

step
game step.
self refreshDisplay

stepTime
^ 16

7.2. Теория stepping

  • startStepping регистрирует морф в очереди тиков Morphic.
  • Каждые stepTime миллисекунд Morphic вызывает step у морфа — аналог одного прохода update() + draw() в игровом движке.
  • 16 мс ≈ 62 кадра/с — плавнее, чем 33 мс (30 FPS).
  • Важно: step у PongGameMorph перекрывает унаследованный step у Morph — в теле сначала логика игры, потом перерисовка позиций.

Схема одного кадра:

7.3. Проверка

Откройте морф снова — мяч должен летать; ракетки пока неподвижны (ввода ещё нет).


Этап 8 — клавиатура и запуск в окне

Цель — W/S и стрелки, пауза по пробелу, окно с заголовком и фокусом клавиатуры.

8.1. События клавиатуры

Протокол events:

handlesKeyboard: evt
^ true

handleKeyDown: anEvent
| keyName |
keyName := anEvent keyName.
pressedKeys add: keyName.
keyName = ' ' ifTrue: [ game togglePause ].
self syncInput

handleKeyUp: anEvent
pressedKeys remove: anEvent keyName ifAbsent: [].
self syncInput

keydown: anEvent
self handleKeyDown: anEvent

keyup: anEvent
self handleKeyUp: anEvent

syncInput
game
leftUp: ((pressedKeys includes: 'W') or: [ pressedKeys includes: 'w' ]);
leftDown: ((pressedKeys includes: 'S') or: [ pressedKeys includes: 's' ]);
rightUp: (pressedKeys includes: 'ArrowUp');
rightDown: (pressedKeys includes: 'ArrowDown')

Разбор:

  • handlesKeyboard:true — морф хочет клавиатурные события.
  • keydown: / keyup: — точки входа Morphic; делегируем в handleKeyDown: / handleKeyUp:.
  • anEvent keyName — строка вроде 'W', 'ArrowUp', ' ' (пробел).
  • Set pressedKeys — множество всех зажатых клавиш; при удержании W морф на каждом keydown не дублирует, а includes: в syncInput даёт стабильный флаг.
  • Каскад game leftUp: ...; leftDown: ... — несколько сообщений одному объекту подряд (рекомендации по стилю).

8.2. Точка входа

Протокол instance creation (метод класса):

open
^ self new openInWorld

Протокол focus:

openInWorld
| window |
window := self openInWindowLabeled: 'Пинг-понг'.
window activate.
self takeKeyboardFocus.
^ window

Разбор:

  • openInWindowLabeled: — морф в отдельном окне с заголовком.
  • takeKeyboardFocusобязательно; без клика по окну стрелки могут не дойти до игры.

8.3. Запуск

Playground:

PongGameMorph open

8.4. Самопроверка

  • W/S двигают левую ракетку, стрелки — правую.
  • Пробел ставит паузу, в статусе текст про паузу.
  • Кнопка "Новая игра" сбрасывает поле и счёт.
  • При 11 очках — сообщение о победителе в statusText.

Этап 9 — загрузка через FileIn

Если вы проходили этапы в Browser, код уже в image. Для переноса на другую машину или быстрой установки — файл LoadSmallPong.st (формат fileOut Pharo), который вы можете сохранить через File → File Out для категории SmallPong.

Вариант 1 — меню

  • File → File In… → выберите ваш LoadSmallPong.st.

Вариант 2 — Playground (подставьте путь к .st на вашем диске)

FileStream readOnlyFileNamed: 'C:\path\to\LoadSmallPong.st' do: [:stream |
stream fileIn ].

Затем:

PongGameMorph open

Не забудьте Save image — FileIn меняет image в памяти; на диск изменения попадут только после сохранения (о языке и image).


Архитектура и поток данных

СлойКлассОтветственность
МодельPongGameПравила, физика, счёт
ПредставлениеPongGameMorph, дочерние морфыПозиции, текст, кнопка
ВводPongGameMorphpressedKeys, syncInput
ДекорPongPlayfieldMorphЛиния посередине поля

Координатная система окна (упрощённо):

Окно PongGameMorph (чёрный фон)
┌────────────────────────────────────────────┐
│ счёт 3 : 7 [Новая игра] │ ← y = 0..50 HUD
│ W/S — левый... │
│ ┌────────────────────────────────────┐ │
│ │ ● ракетка · мяч ракетка ● │ │ ← playfieldOrigin 10@50
│ │ │ центральная линия │ │ │
│ └────────────────────────────────────┘ │
└────────────────────────────────────────────┘

Такое разделение позволяет:

  • тестировать PongGame>>step в Playground без UI;
  • менять цвета и шрифты, не трогая физику;
  • позже добавить сетевого игрока, подменив только syncInput.

Следующий уровень сложности в том же разделе — SmallShooter (несколько сущностей, стрельба).


Полная ревизия

Итоговая структура категории SmallPong:

КлассПротоколы (основные)
PongGameinitialization, accessing, input, game
PongPlayfieldMorphdisplay
PongGameMorphinstance creation, initialization, stepping, actions, display, events, focus

Сверьте с эталонным LoadSmallPong.st:

  • три класса в категории SmallPong;
  • PongGameMorph class>>open и openInWorld с takeKeyboardFocus;
  • winningScore := 11 в initialize;
  • stepTime16;
  • полный step с intersects: и checkWinFor:.

Итоговая самопроверка

  • PongGame new после Accept не даёт MessageNotUnderstood.
  • 100 вызовов step в Playground меняют ballPosition.
  • PongPlayfieldMorph рисует центральную линию.
  • PongGameMorph open открывает окно с заголовком "Пинг-понг".
  • Победа при 11 очках, statusText показывает победителя.
  • Image сохранён после сессии (File → Save).

Частые ошибки

СимптомПричинаЧто сделать
MessageNotUnderstoodМетод не принятAccept (Ctrl+S) в Browser
Клавиши не работаютНет фокусаКлик по окну; проверьте takeKeyboardFocus
Мяч "залипает" в ракеткеНеверный порядок в stepСначала ракетки, потом мяч, потом intersects:
Код пропал после перезапускаНе сохранён imageFile → Save
Два класса PongGameFileIn поверх своей версииУдалите дубликат в Browser или загрузите в чистый image

Подробнее — FAQ раздела Smalltalk, чек-лист.


Дальше


В подборках

ЯзыкиSmalltalk — о разделе, ООП в Smalltalk, Крестики-нолики, Morphic в справочнике.

ИгрыПрактикум разработки игр, Python — Ping Pong, SmallShooter.


Содержание
Освоение главы0%