SmallPong на Morphic — практикум
О чём эта статья
Пошаговый практикум — вы соберёте графическую игру "пинг-понг" в Pharo на Morphic, по шагам в Class Browser, с разбором каждого класса и метода. Логика матча живёт в PongGame, интерфейс — в морфах; связь между ними повторяет идеи MVC из ООП в Smalltalk и десктопной архитектуры.
База: Первая программа, ООП-модель, Morphic. Соседний практикум без игрового цикла — крестики-нолики. Эталон для сверки — полная ревизия в конце статьи или ваш .st-файл после прохождения этапов.
Требования
- Pharo 10+ (рекомендуется 11/12) — первая программа, среда Pharo.
- Class Browser, Playground, Accept (Ctrl+S).
- Желательно пройти крестики-нолики или SmallDesktop на Morphic.
Коротко об оригинале 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, клавиатура |
В Pygame вы сами крутите цикл while running, зовёте pygame.event.get() и рисуете на screen. В Pharo морф подписывается на startStepping, а Morphic периодически шлёт ему step. Модель (PongGame) не знает о морфах — только числа и флаги. Подробный трек с FSM и ИИ — Python — Ping Pong.
Оценка времени — 2–4 часа при прохождении всех этапов в Class Browser.
Карта этапов
| Этап | Фокус | Результат |
|---|---|---|
| 0 | Подготовка Pharo | Категория SmallPong, Browser открыт |
| 1 | PongGame — каркас | Константы поля, initialize, resetGame |
| 2 | Доступ и ввод | Геттеры, флаги leftUp / rightDown |
| 3 | step — стены | Мяч летит и отскакивает от верха/низа |
| 4 | Ракетки и гол | Отскок от ракеток, счёт, gameOver |
| 5 | PongPlayfieldMorph | Центральная линия на поле |
| 6 | PongGameMorph — UI | Поле, ракетки, мяч, счёт, кнопка |
| 7 | Stepping | Живой цикл ~60 FPS |
| 8 | Клавиатура и запуск | PongGameMorph open |
| 9 | FileIn | Загрузка готового .st |
Как проходить практикум
- Создайте категорию SmallPong в Class Browser.
- Идите этапы 0–9 по порядку — после каждого метода жмите Accept (Ctrl+S).
- Запускайте
PongGameMorph openв Playground; кликните по окну для фокуса клавиатуры. - Отмечайте Самопроверку; при расхождении — полная ревизия.
Архитектура
Три класса повторяют схему 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 и категория для классов.
Что сделать
- Установите Pharo 10+ — первая программа.
- Откройте Class Browser (Nautilus / System Browser).
- Создайте категорию классов SmallPong — правая панель →
+→Add category. - Убедитесь, что 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— скорость в пикселях за тик (не за секунду); приstepTime16 мс это ~312 px/с по каждой оси приballSpeed = 5.- Отскок от "стены" — принудительно ставим
ballYна границу и разворачиваемballVYчерезabs/negated.
3.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 gameOver → true.
Этап 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, дочерние морфы | Позиции, текст, кнопка |
| Ввод | PongGameMorph | pressedKeys, syncInput |
| Декор | PongPlayfieldMorph | Линия посередине поля |
Координатная система окна (упрощённо):
Окно PongGameMorph (чёрный фон)
┌────────────────────────────────────────────┐
│ счёт 3 : 7 [Новая игра] │ ← y = 0..50 HUD
│ W/S — левый... │
│ ┌────────────────────────────────────┐ │
│ │ ● ракетка · мяч ракетка ● │ │ ← playfieldOrigin 10@50
│ │ │ центральная линия │ │ │
│ └────────────────────────────────────┘ │
└────────────────────────────────────────────┘
Такое разделение позволяет:
- тестировать
PongGame>>stepв Playground без UI; - менять цвета и шрифты, не трогая физику;
- позже добавить сетевого игрока, подменив только
syncInput.
Следующий уровень сложности в том же разделе — SmallShooter (несколько сущностей, стрельба).
Полная ревизия
Итоговая структура категории SmallPong:
| Класс | Протоколы (основные) |
|---|---|
PongGame | initialization, accessing, input, game |
PongPlayfieldMorph | display |
PongGameMorph | instance creation, initialization, stepping, actions, display, events, focus |
Сверьте с эталонным LoadSmallPong.st:
- три класса в категории
SmallPong; PongGameMorph class>>openиopenInWorldсtakeKeyboardFocus;winningScore := 11вinitialize;stepTime→16;- полный
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: |
| Код пропал после перезапуска | Не сохранён image | File → Save |
Два класса PongGame | FileIn поверх своей версии | Удалите дубликат в Browser или загрузите в чистый image |
Подробнее — FAQ раздела Smalltalk, чек-лист.
Дальше
- Звук при отскоке —
SampledSoundилиPluckedSound(стандартная библиотека). - Вынесите константы в отдельный класс или shared pool.
- ИИ для правой ракетки — следование за
ballY(идея из Python-практикума, этап 12). - Стиль протоколов — Рекомендации по разработке.
- Другой язык, тот же жанр — Практикум игр на Python.
В подборках
Языки — Smalltalk — о разделе, ООП в Smalltalk, Крестики-нолики, Morphic в справочнике.
Игры — Практикум разработки игр, Python — Ping Pong, SmallShooter.