Skip to content

Commit e2523cb

Browse files
committed
v3.3.0
1 parent a4d0e89 commit e2523cb

10 files changed

Lines changed: 687 additions & 339 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@
55
Формат основан на [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
и этот проект придерживается [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [3.3.0]
9+
10+
### Added
11+
-**Мобильный ввод: экранная клавиатура для TextInput** — в Kivy‑режиме SpritePro автоматически запрашивает системную софт‑клавиатуру, когда любое поле `s.TextInput` становится активным. События `on_key_down` и `on_textinput` из Kivy транслируются в `pygame.KEYDOWN` и `pygame.TEXTINPUT`, поэтому `TextInput` обрабатывает ввод на Android так же, как на desktop.
12+
13+
### Changed
14+
- 🔧 **Kivy mobile host**`_KivySpriteProWidget` отслеживает активные `s.TextInput` и открывает/закрывает экранную клавиатуру через `Window.request_keyboard(...)`. Закрытие клавиатуры происходит автоматически при деактивации всех полей ввода (по Enter или при тапе вне поля).
15+
- 🔧 **Версия библиотеки** — релиз обновлён до `3.3.0`.
16+
17+
---
18+
819
## [3.2.0]
920

1021
### Added
@@ -647,6 +658,13 @@
647658

648659
## Планы на будущие версии
649660

661+
## [Unreleased]
662+
663+
- ✨ Улучшенное Kivy-лобби (`use_lobby=True`, `platform="kivy"`) с поддержкой мобильного режима и удобным десктопным тестированием.
664+
- ✨ Автозапуск нескольких Kivy-процессов с лобби при `multiplayer_clients>1` — каждый процесс = отдельный игрок, для локального теста мультиплеера.
665+
- 🎨 Перевод лобби на flex-лейауты SpritePro (`layout_flex_column` / `layout_flex_row`) для аккуратного расположения полей, кнопок и статуса.
666+
- 📚 Обновления документации по `use_lobby=True`, Kivy-режиму и демо `three_clients_move_demo`.
667+
650668
### v1.1.0 - Система инвентаря
651669
- Полноценная система управления предметами
652670
- Drag & Drop интерфейс

docs/networking.md

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,33 @@ TCP‑клиент, который отправляет сообщения и п
4242
- `poll(max_messages=100)` — забирает входящие сообщения.
4343
- `close()` — закрывает соединение.
4444

45+
### Системные события
46+
47+
При использовании `MultiplayerContext` (рекомендуется), хост автоматически получает события о подключении/отключении клиентов через `ctx.poll()`:
48+
49+
- `client_connected` — новый клиент подключился
50+
- `data.client_id` — ID клиента (0, 1, 2...)
51+
- `data.peer` — адрес клиента (например, "127.0.0.1:54321")
52+
- `client_disconnected` — клиент отключился
53+
- `data.client_id` — ID клиента
54+
55+
```python
56+
class MyScene(s.Scene):
57+
def __init__(self, net, role):
58+
super().__init__()
59+
s.multiplayer.init_context(net, role)
60+
self.ctx = s.multiplayer_ctx
61+
62+
def update(self, dt):
63+
for msg in self.ctx.poll():
64+
if msg.get("event") == "client_connected":
65+
client_id = msg["data"]["client_id"]
66+
print(f"Подключился клиент: {client_id}")
67+
elif msg.get("event") == "client_disconnected":
68+
client_id = msg["data"]["client_id"]
69+
print(f"Отключился клиент: {client_id}")
70+
```
71+
4572
### Рекомендуемый запуск: `s.run(..., multiplayer=True)`
4673

4774
Для новых игр удобнее использовать один вход через `s.run(...)`, а multiplayer
@@ -265,7 +292,7 @@ if __name__ == "__main__":
265292
python spritePro/demoGames/local_multiplayer_demo.py --lobby
266293
```
267294

268-
### Сценарий для игрока
295+
### Сценарий для игрока (desktop / pygame)
269296

270297
1. **Один экземпляр (хост)**
271298
- Открыть приложение.
@@ -283,17 +310,34 @@ python spritePro/demoGames/local_multiplayer_demo.py --lobby
283310
- Хост нажимает **«В игру»**.
284311
- У **обоих** игроков закрывается лобби и запускается игра (`multiplayer_main(net, role)`).
285312

313+
### Сценарий для игрока (mobile / Kivy)
314+
315+
- Запуск через `s.run(..., platform="kivy", multiplayer=True, multiplayer_use_lobby=True)` или Kivy-билд.
316+
- Окно лобби и сама игра работают внутри одного Kivy-приложения (SpritePro рендерится в Kivy-виджет).
317+
- Каждый процесс/билд даёт **одного** игрока; для локальных тестов мультиплеера используйте несколько устройств/эмуляторов (по одному клиенту на процесс) **или автоспавн процессов по `clients` (см. ниже)**.
318+
319+
Если указать `multiplayer_clients=1`, такой запуск можно рассматривать как «build‑режим» (один экземпляр клиента, без автоспавна дополнительных окон).
320+
286321
### Для разработчика
287322

288323
- **События лобби:** хост рассылает `start_game` при нажатии «В игру»; клиент при получении `start_game` тоже переходит в игру. Кнопка «Назад» закрывает соединение и возвращает к экрану настройки.
289-
- **Очистка UI:** лобби реализовано как `MultiplayerLobbyScene(s.Scene)`. При выходе из лобби вызывается `on_exit()`: все спрайты лобби (кнопки, поля ввода, текст, в том числе дочерние `text_sprite`) снимаются с регистрации, в игре не остаётся элементов лобби.
324+
- **Очистка UI:** лобби реализовано как `MultiplayerLobbyScene(s.Scene)` и обновляется через `update()`. При выходе из лобби вызывается `on_exit()`: все спрайты лобби (кнопки, поля ввода, текст, в том числе дочерние `text_sprite`) снимаются с регистрации, в игре не остаётся элементов лобби.
290325
- **Использование без run():** можно вызвать лобби вручную после `get_screen()` и передать колбэк перехода в игру:
291326

292327
```python
293328
from spritePro.readyScenes import run_multiplayer_lobby
294329

330+
# pygame / desktop
295331
s.get_screen((480, 540), "Лобби")
296332
run_multiplayer_lobby(lambda net, role: your_multiplayer_main(net, role))
333+
334+
# Kivy / mobile
335+
run_multiplayer_lobby(
336+
lambda net, role: your_multiplayer_main(net, role),
337+
window_size=(480, 540),
338+
title="Лобби",
339+
platform="kivy",
340+
)
297341
```
298342

299343
Если игра уже запускается через `s.run(...)`, то для нового кода обычно удобнее перейти на `s.run(..., multiplayer=True)` и использовать `s.networking.run(...)` только как низкоуровневый runner.
@@ -302,7 +346,10 @@ run_multiplayer_lobby(lambda net, role: your_multiplayer_main(net, role))
302346

303347
## Режимы запуска run()
304348

305-
- **use_lobby=True** (в коде) — одно окно с лобби: настройка (имя, хост/клиент, порт, IP), подключение, roster; у хоста кнопки «Назад» и «В игру», у клиента «Назад». Не используется при запуске через `--quick` / env (тогда роль уже задана).
349+
- **use_lobby=True** (в коде)
350+
- desktop / pygame: одно окно с лобби: настройка (имя, хост/клиент, порт, IP), подключение, roster; у хоста кнопки «Назад» и «В игру», у клиента «Назад».
351+
- mobile / Kivy: одно окно с лобби внутри Kivy-приложения; `size` из `s.run(...)` переиспользуется для окна лобби.
352+
- если `platform="kivy"` и `multiplayer_clients>1`, SpritePro поднимает несколько процессов Kivy с лобби (по одному игроку на процесс) — удобно для локального теста мультиплеера на ПК.
306353
- `--server` — только сервер.
307354
- `--host_mode` — сервер + клиент в одном процессе.
308355
- `--quick` — быстрый запуск (хост + второй клиент).

docs/readySprites.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,40 @@ Ready Sprites are pre-configured sprite classes that solve common game developme
2121
**Использование:**
2222

2323
```python
24-
# Через run() — одно окно с лобби, затем ваша игра
25-
s.run(multiplayer=True, multiplayer_entry=your_multiplayer_main, multiplayer_use_lobby=True)
24+
# Через run() — одно окно с лобби, затем ваша игра (desktop / pygame)
25+
s.run(
26+
multiplayer=True,
27+
multiplayer_entry=your_multiplayer_main,
28+
multiplayer_use_lobby=True,
29+
)
30+
31+
# Мобильный / Kivy-вариант
32+
s.run(
33+
multiplayer=True,
34+
multiplayer_entry=your_multiplayer_main,
35+
multiplayer_use_lobby=True,
36+
platform="kivy",
37+
# multiplayer_clients > 1 на ПК с Kivy поднимет несколько процессов с лобби
38+
# (каждое окно — отдельный игрок, удобно для локального теста).
39+
multiplayer_clients=1,
40+
)
2641
```
2742

2843
```python
29-
# Вручную: после get_screen() вызвать лобби с колбэком
44+
# Вручную: после get_screen() или через Kivy вызвать лобби с колбэком
3045
from spritePro.readyScenes import run_multiplayer_lobby
3146

47+
# pygame / desktop
3248
s.get_screen((480, 540), "Лобби")
3349
run_multiplayer_lobby(lambda net, role: your_multiplayer_main(net, role))
50+
51+
# Kivy / mobile
52+
run_multiplayer_lobby(
53+
lambda net, role: your_multiplayer_main(net, role),
54+
window_size=(480, 540),
55+
title="Лобби",
56+
platform="kivy",
57+
)
3458
```
3559

3660
**Экспорт:** `run_multiplayer_lobby`, `MultiplayerLobbyScene`, `EVENT_START_GAME` из `spritePro.readyScenes`. Подробнее: [Networking — лобби](networking.md#подробная-инструкция-лобби-use_lobbytrue).

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "spritepro"
7-
version = "3.2.4"
7+
version = "3.3.0"
88
authors = [
99
{ name="NeoXider", email="neoxider@gmail.com" },
1010
]

spritePro/__init__.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
from __future__ import annotations
2626

27-
__version__ = "3.2.4"
27+
__version__ = "3.3.0"
2828

2929
import inspect
3030
import os
@@ -1329,6 +1329,17 @@ def _wrapped_setup():
13291329
def _wrapped_scene():
13301330
return _resolve_scene_value(scene, net, role)
13311331

1332+
# Если мы уже внутри Kivy-приложения (лобби / mobile),
1333+
# не запускаем run() повторно, а просто выполняем bootstrap
1334+
# в существующем контексте.
1335+
if os.environ.get("SPRITEPRO_IN_KIVY_APP") == "1":
1336+
_run_bootstrap(
1337+
_wrapped_setup if setup is not None else None,
1338+
_wrapped_scene if scene is not None else None,
1339+
)
1340+
_emit_resize_event(force=True)
1341+
return
1342+
13321343
run(
13331344
_wrapped_setup if setup is not None else None,
13341345
scene=_wrapped_scene if scene is not None else None,
@@ -1360,6 +1371,12 @@ def _wrapped_scene():
13601371
setattr(main_module, _RUN_MULTIPLAYER_ENTRY, entry_callable)
13611372

13621373
os.environ["SPRITEPRO_PLATFORM"] = platform
1374+
# Передаём размер окна в лобби через env, чтобы Kivy/pygame-лобби
1375+
# могли совпадать по size с основной игрой.
1376+
try:
1377+
os.environ["SPRITEPRO_LOBBY_SIZE"] = f"{int(size[0])},{int(size[1])}"
1378+
except Exception:
1379+
pass
13631380
_networking.run(
13641381
argv=filtered_multiplayer_argv,
13651382
entry=entry_name,

spritePro/demoGames/three_clients_move_demo.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ def _color_for_id(client_id: int) -> tuple[int, int, int]:
5151
class ThreeClientsMoveScene(s.Scene):
5252
def __init__(self, net: s.NetClient, role: str) -> None:
5353
super().__init__()
54-
s.multiplayer.init_context(net, role)
54+
# Если контекст уже инициализирован (например, через лобби), переиспользуем его,
55+
# иначе инициализируем заново для quick/terminal-режима.
56+
if s.multiplayer_ctx is None:
57+
s.multiplayer.init_context(net, role)
5558
self.ctx = s.multiplayer_ctx
5659
self.hint = s.TextSprite(
5760
"WASD — move | IDs above players", 20, (200, 200, 200), (450, 30), scene=self
@@ -152,6 +155,7 @@ def main(default_platform: str = "kivy") -> None:
152155
multiplayer_argv=sys.argv[1:],
153156
multiplayer_clients=3,
154157
multiplayer_client_spawn_delay=2,
158+
multiplayer_use_lobby= True
155159
)
156160

157161

spritePro/mobile.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def __init__(
8686
self._active_touch_id: str | None = None
8787
self._last_touch_pos: tuple[int, int] | None = None
8888
self._bootstrap_wait_frames = 0
89+
self._keyboard = None
8990
self._fatal_error: str | None = None
9091

9192
with self.canvas:
@@ -261,6 +262,114 @@ def on_touch_up(self, touch):
261262
self._last_touch_pos = None
262263
return True
263264

265+
def _map_kivy_key_to_pygame(self, keycode, modifiers) -> tuple[int, int]:
266+
key_value, key_name = keycode
267+
name = (key_name or "").lower()
268+
mapping = {
269+
"enter": pygame.K_RETURN,
270+
"return": pygame.K_RETURN,
271+
"kp_enter": pygame.K_RETURN,
272+
"backspace": pygame.K_BACKSPACE,
273+
"escape": pygame.K_ESCAPE,
274+
"esc": pygame.K_ESCAPE,
275+
"space": pygame.K_SPACE,
276+
"spacebar": pygame.K_SPACE,
277+
}
278+
if name in mapping:
279+
pg_key = mapping[name]
280+
elif len(name) == 1:
281+
attr_name = f"K_{name}"
282+
pg_key = getattr(pygame, attr_name, key_value)
283+
else:
284+
pg_key = key_value
285+
286+
mod = 0
287+
if "shift" in modifiers:
288+
mod |= pygame.KMOD_SHIFT
289+
if "ctrl" in modifiers or "control" in modifiers:
290+
mod |= pygame.KMOD_CTRL
291+
if "alt" in modifiers:
292+
mod |= pygame.KMOD_ALT
293+
if "meta" in modifiers or "cmd" in modifiers or "super" in modifiers:
294+
mod |= pygame.KMOD_META
295+
return pg_key, mod
296+
297+
def _open_soft_keyboard(self) -> None:
298+
if self._keyboard is not None:
299+
return
300+
try:
301+
from kivy.core.window import Window
302+
except Exception:
303+
return
304+
keyboard = Window.request_keyboard(self._on_keyboard_closed, self)
305+
if keyboard is None:
306+
return
307+
self._keyboard = keyboard
308+
try:
309+
self._keyboard.bind(
310+
on_key_down=self._on_key_down,
311+
on_textinput=self._on_textinput,
312+
)
313+
except Exception:
314+
self._keyboard = None
315+
316+
def _close_soft_keyboard(self) -> None:
317+
kb = self._keyboard
318+
if kb is None:
319+
return
320+
try:
321+
kb.unbind(on_key_down=self._on_key_down, on_textinput=self._on_textinput)
322+
except Exception:
323+
pass
324+
try:
325+
kb.release()
326+
except Exception:
327+
pass
328+
self._keyboard = None
329+
330+
def _on_keyboard_closed(self, *_args) -> None:
331+
self._keyboard = None
332+
333+
def _on_key_down(self, keyboard, keycode, text, modifiers):
334+
pg_key, pg_mod = self._map_kivy_key_to_pygame(keycode, modifiers or [])
335+
event = pygame.event.Event(
336+
pygame.KEYDOWN,
337+
{
338+
"key": pg_key,
339+
"mod": pg_mod,
340+
"unicode": text or "",
341+
},
342+
)
343+
self._event_queue.append(event)
344+
return True
345+
346+
def _on_textinput(self, keyboard, text: str):
347+
if not text:
348+
return False
349+
event = pygame.event.Event(
350+
pygame.TEXTINPUT,
351+
{
352+
"text": text,
353+
},
354+
)
355+
self._event_queue.append(event)
356+
return True
357+
358+
def _sync_soft_keyboard_state(self) -> None:
359+
try:
360+
active_inputs = [
361+
ti
362+
for ti in s.get_sprites_by_class(s.TextInput, active_only=True)
363+
if getattr(ti, "is_active", False)
364+
]
365+
except Exception:
366+
active_inputs = []
367+
368+
if active_inputs and self._keyboard is None:
369+
self._open_soft_keyboard()
370+
elif not active_inputs and self._keyboard is not None:
371+
self._close_soft_keyboard()
372+
264373
def _tick(self, _dt: float) -> None:
265374
if self._surface is None:
266375
self._on_resize()
@@ -293,6 +402,8 @@ def _tick(self, _dt: float) -> None:
293402
self._draw_fatal_error()
294403
return
295404

405+
self._sync_soft_keyboard_state()
406+
296407
self._present_surface()
297408

298409
return _KivySpriteProWidget, App

0 commit comments

Comments
 (0)