pycoode 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pycoode/__init__.py +37 -0
- pycoode/audio.py +69 -0
- pycoode/core.py +455 -0
- pycoode/demo.py +59 -0
- pycoode/gfx.py +95 -0
- pycoode/py.typed +0 -0
- pycoode/scene.py +65 -0
- pycoode/sprites.py +161 -0
- pycoode/text.py +103 -0
- pycoode/utils.py +137 -0
- pycoode-1.0.0.dist-info/METADATA +221 -0
- pycoode-1.0.0.dist-info/RECORD +14 -0
- pycoode-1.0.0.dist-info/WHEEL +5 -0
- pycoode-1.0.0.dist-info/top_level.txt +1 -0
pycoode/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""pycoode —— 让做游戏像写一行代码一样简单
|
|
2
|
+
|
|
3
|
+
基于 pygame 封装: 自动处理窗口创建、中文字体、高 DPI、
|
|
4
|
+
分辨率适配、游戏循环、输入、精灵、音效和场景。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .core import Game, run, keycode
|
|
8
|
+
from .sprites import Sprite, load_image
|
|
9
|
+
from .scene import Scene
|
|
10
|
+
from .audio import (
|
|
11
|
+
play, music, stop_music, pause_music, resume_music,
|
|
12
|
+
set_music_volume, is_music_playing,
|
|
13
|
+
)
|
|
14
|
+
from .utils import (
|
|
15
|
+
Vector, to_color, clamp, lerp, dist, angle,
|
|
16
|
+
rand, randint, choice, chance,
|
|
17
|
+
)
|
|
18
|
+
from .demo import demo
|
|
19
|
+
|
|
20
|
+
__version__ = "1.0.0"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def sprite(path, x=0, y=0, **kw):
|
|
24
|
+
"""加载精灵: pc.sprite("hero.png", 400, 300)"""
|
|
25
|
+
return Sprite.load(path, x, y, **kw)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"Game", "run", "demo", "keycode",
|
|
30
|
+
"Sprite", "sprite", "load_image",
|
|
31
|
+
"Scene",
|
|
32
|
+
"play", "music", "stop_music", "pause_music", "resume_music",
|
|
33
|
+
"set_music_volume", "is_music_playing",
|
|
34
|
+
"Vector", "to_color", "clamp", "lerp", "dist", "angle",
|
|
35
|
+
"rand", "randint", "choice", "chance",
|
|
36
|
+
"__version__",
|
|
37
|
+
]
|
pycoode/audio.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""pycoode.audio —— 音效与背景音乐,一行播放
|
|
2
|
+
|
|
3
|
+
没有音频设备时所有函数静默降级,不会让游戏崩掉。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pygame
|
|
7
|
+
|
|
8
|
+
_sound_cache = {}
|
|
9
|
+
_mixer_ok = True
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _ensure_mixer():
|
|
13
|
+
global _mixer_ok
|
|
14
|
+
if not _mixer_ok:
|
|
15
|
+
return False
|
|
16
|
+
try:
|
|
17
|
+
if not pygame.mixer.get_init():
|
|
18
|
+
pygame.mixer.init()
|
|
19
|
+
return True
|
|
20
|
+
except Exception:
|
|
21
|
+
_mixer_ok = False
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def play(path, volume=1.0):
|
|
26
|
+
"""播放音效(可重复调用,自动缓存)"""
|
|
27
|
+
if not _ensure_mixer():
|
|
28
|
+
return None
|
|
29
|
+
key = str(path)
|
|
30
|
+
if key not in _sound_cache:
|
|
31
|
+
_sound_cache[key] = pygame.mixer.Sound(key)
|
|
32
|
+
s = _sound_cache[key]
|
|
33
|
+
s.set_volume(max(0.0, min(1.0, volume)))
|
|
34
|
+
s.play()
|
|
35
|
+
return s
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def music(path, loop=True, volume=0.6, fade_ms=0):
|
|
39
|
+
"""播放背景音乐(默认循环)"""
|
|
40
|
+
if not _ensure_mixer():
|
|
41
|
+
return
|
|
42
|
+
pygame.mixer.music.load(str(path))
|
|
43
|
+
pygame.mixer.music.set_volume(max(0.0, min(1.0, volume)))
|
|
44
|
+
pygame.mixer.music.play(-1 if loop else 0, fade_ms=fade_ms)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def stop_music(fade_ms=0):
|
|
48
|
+
if not _ensure_mixer():
|
|
49
|
+
return
|
|
50
|
+
pygame.mixer.music.fadeout(fade_ms) if fade_ms else pygame.mixer.music.stop()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def pause_music():
|
|
54
|
+
if _ensure_mixer():
|
|
55
|
+
pygame.mixer.music.pause()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resume_music():
|
|
59
|
+
if _ensure_mixer():
|
|
60
|
+
pygame.mixer.music.unpause()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def set_music_volume(v):
|
|
64
|
+
if _ensure_mixer():
|
|
65
|
+
pygame.mixer.music.set_volume(max(0.0, min(1.0, v)))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_music_playing():
|
|
69
|
+
return _ensure_mixer() and pygame.mixer.music.get_busy()
|
pycoode/core.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""pycoode.core —— Game 类与一行入口 run()
|
|
2
|
+
|
|
3
|
+
解决"适配问题"的三个设计:
|
|
4
|
+
1. 虚拟画布 + 信箱缩放: 游戏永远按设计分辨率(如 800x600)写坐标,
|
|
5
|
+
窗口任意拉伸都不变形、不错位;
|
|
6
|
+
2. Windows 高 DPI 适配, 笔记本缩放 150% 也不会模糊偏移;
|
|
7
|
+
3. 中文自动匹配字体, text("你好") 不开字体文件也能显示。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import pygame
|
|
14
|
+
|
|
15
|
+
from .gfx import DrawingMixin
|
|
16
|
+
from .utils import to_color
|
|
17
|
+
|
|
18
|
+
# Windows 高 DPI: 必须在 pygame.init() 之前设置
|
|
19
|
+
if sys.platform.startswith("win"):
|
|
20
|
+
os.environ.setdefault("SDL_WINDOWS_DPI_AWARENESS", "per monitor")
|
|
21
|
+
|
|
22
|
+
# 常用按键名 -> pygame 键值, 让 g.key_held("space") 这种写法可用
|
|
23
|
+
KEY_NAMES = {
|
|
24
|
+
"space": pygame.K_SPACE, "enter": pygame.K_RETURN, "return": pygame.K_RETURN,
|
|
25
|
+
"esc": pygame.K_ESCAPE, "escape": pygame.K_ESCAPE, "tab": pygame.K_TAB,
|
|
26
|
+
"up": pygame.K_UP, "down": pygame.K_DOWN, "left": pygame.K_LEFT, "right": pygame.K_RIGHT,
|
|
27
|
+
"shift": pygame.K_LSHIFT, "ctrl": pygame.K_LCTRL, "control": pygame.K_LCTRL,
|
|
28
|
+
"alt": pygame.K_LALT, "backspace": pygame.K_BACKSPACE, "delete": pygame.K_DELETE,
|
|
29
|
+
"home": pygame.K_HOME, "end": pygame.K_END, "pageup": pygame.K_PAGEUP, "pagedown": pygame.K_PAGEDOWN,
|
|
30
|
+
"insert": pygame.K_INSERT, "capslock": pygame.K_CAPSLOCK,
|
|
31
|
+
**{chr(c): c for c in range(ord("a"), ord("z") + 1)},
|
|
32
|
+
**{d: getattr(pygame, f"K_{d}") for d in "0123456789"},
|
|
33
|
+
**{f"f{i}": getattr(pygame, f"K_F{i}") for i in range(1, 13)},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def keycode(key):
|
|
38
|
+
"""把 'space'/'a'/pygame.K_SPACE 统一成 pygame 键值; 无法识别返回 None"""
|
|
39
|
+
if isinstance(key, int):
|
|
40
|
+
return key
|
|
41
|
+
return KEY_NAMES.get(str(key).strip().lower())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# 常用窗口尺寸预设: pc.run(size="hd")
|
|
45
|
+
SIZE_PRESETS = {
|
|
46
|
+
"small": (640, 480),
|
|
47
|
+
"md": (800, 600),
|
|
48
|
+
"large": (1024, 768),
|
|
49
|
+
"square": (600, 600),
|
|
50
|
+
"hd": (1280, 720),
|
|
51
|
+
"fullhd": (1920, 1080),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_size(size, fallback):
|
|
56
|
+
"""把 'hd' / (1280, 720) 统一成 (宽, 高)"""
|
|
57
|
+
if size is None:
|
|
58
|
+
return fallback
|
|
59
|
+
if isinstance(size, str):
|
|
60
|
+
key = size.strip().lower()
|
|
61
|
+
if key in SIZE_PRESETS:
|
|
62
|
+
return SIZE_PRESETS[key]
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"未知尺寸预设 {size!r}, 可用: {', '.join(sorted(SIZE_PRESETS))}")
|
|
65
|
+
if isinstance(size, (tuple, list)) and len(size) == 2:
|
|
66
|
+
return (int(size[0]), int(size[1]))
|
|
67
|
+
raise TypeError("size 需要是 (宽, 高) 元组或预设名字, 如 size='hd'")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _desktop_size():
|
|
71
|
+
"""当前桌面分辨率; 取不到返回 None(如 dummy 驱动 / 无头环境)"""
|
|
72
|
+
try:
|
|
73
|
+
modes = pygame.display.list_modes()
|
|
74
|
+
if modes and modes != -1 and modes[0][0] > 0:
|
|
75
|
+
return modes[0]
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Game(DrawingMixin):
|
|
82
|
+
"""pycoode 游戏主类。
|
|
83
|
+
|
|
84
|
+
最简用法:
|
|
85
|
+
import pycoode as pc
|
|
86
|
+
pc.run() # 直接弹出窗口
|
|
87
|
+
pc.run(draw=lambda g: g.text("Hi", 10, 10))
|
|
88
|
+
|
|
89
|
+
完整用法:
|
|
90
|
+
g = pc.Game(800, 600, title="我的游戏", fps=60)
|
|
91
|
+
g.on_update = lambda g, dt: ...
|
|
92
|
+
g.on_draw = lambda g: ...
|
|
93
|
+
g.run()
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, width=800, height=600, title="pycoode", fps=60,
|
|
97
|
+
background="dark", resizable=True, fullscreen=False,
|
|
98
|
+
show_fps=False, icon=None, quit_key="escape",
|
|
99
|
+
size=None, fit_screen=False, mouse_visible=True):
|
|
100
|
+
pygame.init()
|
|
101
|
+
try:
|
|
102
|
+
pygame.font.init()
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
try:
|
|
106
|
+
pygame.mixer.init()
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
# 窗口大小: size 预设/元组优先, 其次 fit_screen 自动收缩到屏幕内
|
|
111
|
+
if size is not None:
|
|
112
|
+
width, height = _parse_size(size, (width, height))
|
|
113
|
+
if fit_screen:
|
|
114
|
+
desktop = _desktop_size()
|
|
115
|
+
if desktop:
|
|
116
|
+
width = min(width, max(320, int(desktop[0] * 0.9)))
|
|
117
|
+
height = min(height, max(240, int(desktop[1] * 0.9)))
|
|
118
|
+
|
|
119
|
+
self.resizable = bool(resizable)
|
|
120
|
+
self._fullscreen = bool(fullscreen)
|
|
121
|
+
flags = 0
|
|
122
|
+
if self.resizable:
|
|
123
|
+
flags |= pygame.RESIZABLE
|
|
124
|
+
if self._fullscreen:
|
|
125
|
+
flags |= pygame.FULLSCREEN
|
|
126
|
+
self.screen = pygame.display.set_mode((int(width), int(height)), flags)
|
|
127
|
+
pygame.display.set_caption(str(title))
|
|
128
|
+
if icon:
|
|
129
|
+
try:
|
|
130
|
+
pygame.display.set_icon(pygame.image.load(str(icon)))
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
# 设计分辨率(虚拟画布) vs 真实窗口, 两者解耦是"零适配"的关键
|
|
135
|
+
self.width = int(width)
|
|
136
|
+
self.height = int(height)
|
|
137
|
+
self.canvas = pygame.Surface((self.width, self.height))
|
|
138
|
+
|
|
139
|
+
self.title = str(title)
|
|
140
|
+
self.fps = int(fps)
|
|
141
|
+
self.background = background
|
|
142
|
+
self.quit_key = keycode(quit_key) or pygame.K_ESCAPE
|
|
143
|
+
self.show_fps = show_fps
|
|
144
|
+
|
|
145
|
+
# 鼠标指针可见性(游戏里常隐藏)
|
|
146
|
+
self.mouse_visible = bool(mouse_visible)
|
|
147
|
+
try:
|
|
148
|
+
pygame.mouse.set_visible(self.mouse_visible)
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
self.running = False
|
|
153
|
+
self.clock = pygame.time.Clock()
|
|
154
|
+
self.dt = 1.0 / self.fps
|
|
155
|
+
self.time = 0.0
|
|
156
|
+
self.frame = 0
|
|
157
|
+
|
|
158
|
+
# ---- 输入状态 ----
|
|
159
|
+
self.keys = set() # 当前按住
|
|
160
|
+
self._pressed = set() # 本帧刚按下
|
|
161
|
+
self._released = set() # 本帧刚松开
|
|
162
|
+
self.mouse_x = 0.0
|
|
163
|
+
self.mouse_y = 0.0
|
|
164
|
+
self.mouse_buttons = set()
|
|
165
|
+
self._mouse_pressed = set()
|
|
166
|
+
self._mouse_released = set()
|
|
167
|
+
self.touches = [] # [(x, y, finger_id), ...] 触摸点
|
|
168
|
+
|
|
169
|
+
# ---- 回调 / 场景 ----
|
|
170
|
+
self.on_update = None
|
|
171
|
+
self.on_draw = None
|
|
172
|
+
self.on_key = None # on_key(key)
|
|
173
|
+
self.on_click = None # on_click(x, y, button)
|
|
174
|
+
self.scene = None
|
|
175
|
+
self._scene_stack = []
|
|
176
|
+
self._scene_ops = [] # 本帧待执行的场景切换, 帧初统一应用
|
|
177
|
+
|
|
178
|
+
# ========================================================================
|
|
179
|
+
# 主循环
|
|
180
|
+
# ========================================================================
|
|
181
|
+
|
|
182
|
+
def run(self):
|
|
183
|
+
"""启动游戏主循环(阻塞,直到退出)"""
|
|
184
|
+
self.running = True
|
|
185
|
+
try:
|
|
186
|
+
while self.running:
|
|
187
|
+
dt = self.clock.tick(self.fps) / 1000.0
|
|
188
|
+
self._frame(min(dt, 0.1)) # 掉帧时 dt 上限 100ms, 防穿墙
|
|
189
|
+
finally:
|
|
190
|
+
self._shutdown()
|
|
191
|
+
|
|
192
|
+
def step(self, dt=None):
|
|
193
|
+
"""手动推进一步(测试 / 自己写循环时用)"""
|
|
194
|
+
if dt is None:
|
|
195
|
+
dt = min(self.clock.tick(self.fps) / 1000.0, 0.1)
|
|
196
|
+
self._frame(dt)
|
|
197
|
+
|
|
198
|
+
def _frame(self, dt):
|
|
199
|
+
self.dt = dt
|
|
200
|
+
self.time += dt
|
|
201
|
+
self.frame += 1
|
|
202
|
+
self._apply_scene_ops()
|
|
203
|
+
self._poll_events()
|
|
204
|
+
self._update(dt)
|
|
205
|
+
self._draw()
|
|
206
|
+
|
|
207
|
+
def _shutdown(self):
|
|
208
|
+
try:
|
|
209
|
+
pygame.quit()
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
def stop(self):
|
|
214
|
+
"""退出游戏"""
|
|
215
|
+
self.running = False
|
|
216
|
+
|
|
217
|
+
def _update(self, dt):
|
|
218
|
+
if self.scene is not None:
|
|
219
|
+
self.scene.update(self, dt)
|
|
220
|
+
if self.on_update:
|
|
221
|
+
self.on_update(self, dt)
|
|
222
|
+
|
|
223
|
+
def _draw(self):
|
|
224
|
+
self.canvas.fill(to_color(self.background))
|
|
225
|
+
if self.scene is not None:
|
|
226
|
+
self.scene.draw(self)
|
|
227
|
+
if self.on_draw:
|
|
228
|
+
self.on_draw(self)
|
|
229
|
+
if self.show_fps:
|
|
230
|
+
self.text(f"FPS {self.clock.get_fps():.0f}", 8, 8, size=16,
|
|
231
|
+
color=(120, 255, 120))
|
|
232
|
+
self._present()
|
|
233
|
+
|
|
234
|
+
# ========================================================================
|
|
235
|
+
# 缩放适配: 虚拟画布 -> 真实窗口 (信箱式, 保持比例)
|
|
236
|
+
# ========================================================================
|
|
237
|
+
|
|
238
|
+
def _view(self):
|
|
239
|
+
"""返回 (scale, offset_x, offset_y)"""
|
|
240
|
+
sw, sh = self.screen.get_size()
|
|
241
|
+
if sw == 0 or sh == 0:
|
|
242
|
+
return 1.0, 0, 0
|
|
243
|
+
scale = min(sw / self.width, sh / self.height)
|
|
244
|
+
w, h = self.width * scale, self.height * scale
|
|
245
|
+
return scale, (sw - w) / 2.0, (sh - h) / 2.0
|
|
246
|
+
|
|
247
|
+
def _present(self):
|
|
248
|
+
scale, ox, oy = self._view()
|
|
249
|
+
w = max(1, int(self.width * scale))
|
|
250
|
+
h = max(1, int(self.height * scale))
|
|
251
|
+
self.screen.fill((0, 0, 0))
|
|
252
|
+
self.screen.blit(pygame.transform.smoothscale(self.canvas, (w, h)),
|
|
253
|
+
(int(ox), int(oy)))
|
|
254
|
+
pygame.display.flip()
|
|
255
|
+
|
|
256
|
+
def _to_canvas(self, sx, sy):
|
|
257
|
+
"""屏幕坐标 -> 游戏内坐标"""
|
|
258
|
+
scale, ox, oy = self._view()
|
|
259
|
+
return (sx - ox) / scale, (sy - oy) / scale
|
|
260
|
+
|
|
261
|
+
# ========================================================================
|
|
262
|
+
# 窗口控制: 改窗口大小 / 全屏 / 鼠标指针
|
|
263
|
+
# ========================================================================
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def screen_size(self):
|
|
267
|
+
"""真实窗口的像素大小(区别于游戏设计分辨率 g.width/g.height)"""
|
|
268
|
+
return self.screen.get_size()
|
|
269
|
+
|
|
270
|
+
def _recreate_window(self, w, h, fullscreen):
|
|
271
|
+
flags = 0
|
|
272
|
+
if fullscreen:
|
|
273
|
+
flags |= pygame.FULLSCREEN
|
|
274
|
+
elif self.resizable:
|
|
275
|
+
flags |= pygame.RESIZABLE
|
|
276
|
+
self.screen = pygame.display.set_mode((int(w), int(h)), flags)
|
|
277
|
+
self._fullscreen = bool(fullscreen)
|
|
278
|
+
|
|
279
|
+
def set_window_size(self, w, h):
|
|
280
|
+
"""运行时改变窗口大小;游戏设计分辨率不变,画面自动缩放适配"""
|
|
281
|
+
self._recreate_window(w, h, False)
|
|
282
|
+
|
|
283
|
+
def toggle_fullscreen(self):
|
|
284
|
+
"""全屏 / 窗口模式切换(快捷键 F11)"""
|
|
285
|
+
self._recreate_window(self.width, self.height, not self._fullscreen)
|
|
286
|
+
|
|
287
|
+
def set_mouse_visible(self, visible):
|
|
288
|
+
"""显示 / 隐藏鼠标指针(游戏里常用)"""
|
|
289
|
+
self.mouse_visible = bool(visible)
|
|
290
|
+
try:
|
|
291
|
+
pygame.mouse.set_visible(self.mouse_visible)
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
# ========================================================================
|
|
296
|
+
# 输入查询
|
|
297
|
+
# ========================================================================
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def mouse_pos(self):
|
|
301
|
+
return (self.mouse_x, self.mouse_y)
|
|
302
|
+
|
|
303
|
+
def key_held(self, key):
|
|
304
|
+
"""按键当前是否按住"""
|
|
305
|
+
k = keycode(key)
|
|
306
|
+
return k is not None and k in self.keys
|
|
307
|
+
|
|
308
|
+
def key_pressed(self, key):
|
|
309
|
+
"""按键本帧是否刚按下(按下瞬间只触发一帧)"""
|
|
310
|
+
k = keycode(key)
|
|
311
|
+
return k is not None and k in self._pressed
|
|
312
|
+
|
|
313
|
+
def key_released(self, key):
|
|
314
|
+
"""按键本帧是否刚松开"""
|
|
315
|
+
k = keycode(key)
|
|
316
|
+
return k is not None and k in self._released
|
|
317
|
+
|
|
318
|
+
def mouse_held(self, button=1):
|
|
319
|
+
return button in self.mouse_buttons
|
|
320
|
+
|
|
321
|
+
def mouse_pressed(self, button=1):
|
|
322
|
+
return button in self._mouse_pressed
|
|
323
|
+
|
|
324
|
+
def mouse_released(self, button=1):
|
|
325
|
+
return button in self._mouse_released
|
|
326
|
+
|
|
327
|
+
# ========================================================================
|
|
328
|
+
# 场景管理
|
|
329
|
+
# ========================================================================
|
|
330
|
+
|
|
331
|
+
def switch_scene(self, scene):
|
|
332
|
+
self._scene_ops.append(("switch", scene))
|
|
333
|
+
|
|
334
|
+
def push_scene(self, scene):
|
|
335
|
+
self._scene_ops.append(("push", scene))
|
|
336
|
+
|
|
337
|
+
def pop_scene(self):
|
|
338
|
+
self._scene_ops.append(("pop", None))
|
|
339
|
+
|
|
340
|
+
def _apply_scene_ops(self):
|
|
341
|
+
while self._scene_ops:
|
|
342
|
+
op, arg = self._scene_ops.pop(0)
|
|
343
|
+
if op == "switch":
|
|
344
|
+
self._exit_scene(self.scene)
|
|
345
|
+
self._enter_scene(arg)
|
|
346
|
+
elif op == "push":
|
|
347
|
+
if self.scene is not None:
|
|
348
|
+
self._scene_stack.append(self.scene)
|
|
349
|
+
self._enter_scene(arg)
|
|
350
|
+
elif op == "pop":
|
|
351
|
+
self._exit_scene(self.scene)
|
|
352
|
+
self._enter_scene(self._scene_stack.pop() if self._scene_stack else None)
|
|
353
|
+
|
|
354
|
+
def _enter_scene(self, scene):
|
|
355
|
+
self.scene = scene
|
|
356
|
+
if scene is not None:
|
|
357
|
+
scene.game = self
|
|
358
|
+
scene.on_enter(self)
|
|
359
|
+
|
|
360
|
+
def _exit_scene(self, scene):
|
|
361
|
+
if scene is not None:
|
|
362
|
+
scene.on_exit(self)
|
|
363
|
+
|
|
364
|
+
# ========================================================================
|
|
365
|
+
# 事件
|
|
366
|
+
# ========================================================================
|
|
367
|
+
|
|
368
|
+
def _poll_events(self):
|
|
369
|
+
self._pressed.clear()
|
|
370
|
+
self._released.clear()
|
|
371
|
+
self._mouse_pressed.clear()
|
|
372
|
+
self._mouse_released.clear()
|
|
373
|
+
|
|
374
|
+
for e in pygame.event.get():
|
|
375
|
+
if e.type == pygame.QUIT:
|
|
376
|
+
self.stop()
|
|
377
|
+
elif e.type == pygame.KEYDOWN:
|
|
378
|
+
if e.key == self.quit_key:
|
|
379
|
+
self.stop()
|
|
380
|
+
continue
|
|
381
|
+
self.keys.add(e.key)
|
|
382
|
+
self._pressed.add(e.key)
|
|
383
|
+
if e.key == pygame.K_F11:
|
|
384
|
+
self.toggle_fullscreen()
|
|
385
|
+
if self.scene is not None:
|
|
386
|
+
self.scene.handle_key(self, e.key)
|
|
387
|
+
if self.on_key:
|
|
388
|
+
self.on_key(e.key)
|
|
389
|
+
elif e.type == pygame.KEYUP:
|
|
390
|
+
self.keys.discard(e.key)
|
|
391
|
+
self._released.add(e.key)
|
|
392
|
+
elif e.type == pygame.MOUSEMOTION:
|
|
393
|
+
self.mouse_x, self.mouse_y = self._to_canvas(*e.pos)
|
|
394
|
+
elif e.type == pygame.MOUSEBUTTONDOWN:
|
|
395
|
+
x, y = self._to_canvas(*e.pos)
|
|
396
|
+
self.mouse_x, self.mouse_y = x, y
|
|
397
|
+
self.mouse_buttons.add(e.button)
|
|
398
|
+
self._mouse_pressed.add(e.button)
|
|
399
|
+
if self.scene is not None:
|
|
400
|
+
self.scene.handle_click(self, x, y, e.button)
|
|
401
|
+
if self.on_click:
|
|
402
|
+
self.on_click(x, y, e.button)
|
|
403
|
+
elif e.type == pygame.MOUSEBUTTONUP:
|
|
404
|
+
self.mouse_buttons.discard(e.button)
|
|
405
|
+
self._mouse_released.add(e.button)
|
|
406
|
+
elif e.type == pygame.FINGERDOWN:
|
|
407
|
+
self._touch_add(e)
|
|
408
|
+
elif e.type == pygame.FINGERMOTION:
|
|
409
|
+
self._touch_add(e)
|
|
410
|
+
elif e.type == pygame.FINGERUP:
|
|
411
|
+
self.touches = [t for t in self.touches if t[2] != e.finger_id]
|
|
412
|
+
elif e.type == pygame.VIDEORESIZE:
|
|
413
|
+
self._recreate_window(e.w, e.h, self._fullscreen)
|
|
414
|
+
|
|
415
|
+
def _touch_add(self, e):
|
|
416
|
+
sw, sh = self.screen.get_size()
|
|
417
|
+
x, y = self._to_canvas(e.x * sw, e.y * sh)
|
|
418
|
+
self.touches = [t for t in self.touches if t[2] != e.finger_id]
|
|
419
|
+
self.touches.append((x, y, e.finger_id))
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def run(update=None, draw=None, on_key=None, on_click=None, scene=None,
|
|
423
|
+
width=800, height=600, title="pycoode", fps=60, background="dark",
|
|
424
|
+
fullscreen=False, resizable=True, show_fps=False, icon=None,
|
|
425
|
+
quit_key="escape", size=None, fit_screen=False, mouse_visible=True):
|
|
426
|
+
"""一行启动游戏。
|
|
427
|
+
|
|
428
|
+
pc.run() # 空窗口
|
|
429
|
+
pc.run(draw=lambda g: g.text("Hi", 10, 10)) # 画点东西
|
|
430
|
+
pc.run(update=my_update, draw=my_draw, width=960, height=640)
|
|
431
|
+
pc.run(scene=Menu()) # 用场景管理
|
|
432
|
+
pc.run(size="hd") # 预设窗口大小
|
|
433
|
+
pc.run(size=(1280, 720)) # 或直接给 (宽, 高)
|
|
434
|
+
pc.run(fit_screen=True) # 自动适应你的屏幕
|
|
435
|
+
pc.run(mouse_visible=False) # 隐藏鼠标指针
|
|
436
|
+
"""
|
|
437
|
+
g = Game(width=width, height=height, title=title, fps=fps,
|
|
438
|
+
background=background, fullscreen=fullscreen,
|
|
439
|
+
resizable=resizable, show_fps=show_fps, icon=icon,
|
|
440
|
+
quit_key=quit_key, size=size, fit_screen=fit_screen,
|
|
441
|
+
mouse_visible=mouse_visible)
|
|
442
|
+
if scene is not None:
|
|
443
|
+
g.scene = scene() if isinstance(scene, type) else scene
|
|
444
|
+
g.scene.game = g
|
|
445
|
+
g.scene.on_enter(g)
|
|
446
|
+
if update is not None:
|
|
447
|
+
g.on_update = update
|
|
448
|
+
if draw is not None:
|
|
449
|
+
g.on_draw = draw
|
|
450
|
+
if on_key is not None:
|
|
451
|
+
g.on_key = on_key
|
|
452
|
+
if on_click is not None:
|
|
453
|
+
g.on_click = on_click
|
|
454
|
+
g.run()
|
|
455
|
+
return g
|
pycoode/demo.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""pycoode.demo —— 内置演示小游戏
|
|
2
|
+
|
|
3
|
+
import pycoode
|
|
4
|
+
pycoode.demo()
|
|
5
|
+
|
|
6
|
+
方向键 / WASD 移动青色方块,吃掉金色金币,ESC 退出。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from . import utils
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def demo(width=800, height=600):
|
|
13
|
+
import pycoode as pc
|
|
14
|
+
|
|
15
|
+
player = pc.Sprite.solid("cyan", 36, 36, width / 2, height / 2)
|
|
16
|
+
coins = []
|
|
17
|
+
score = 0
|
|
18
|
+
|
|
19
|
+
def spawn_coin():
|
|
20
|
+
coins.append(pc.Sprite.solid(
|
|
21
|
+
"gold", 18, 18,
|
|
22
|
+
utils.rand(40, width - 40), utils.rand(40, height - 40),
|
|
23
|
+
))
|
|
24
|
+
|
|
25
|
+
for _ in range(5):
|
|
26
|
+
spawn_coin()
|
|
27
|
+
|
|
28
|
+
def update(g, dt):
|
|
29
|
+
nonlocal score
|
|
30
|
+
speed = 320
|
|
31
|
+
dx = int(g.key_held("right")) - int(g.key_held("left"))
|
|
32
|
+
dy = int(g.key_held("down")) - int(g.key_held("up"))
|
|
33
|
+
if g.key_held("d"):
|
|
34
|
+
dx += 1
|
|
35
|
+
if g.key_held("a"):
|
|
36
|
+
dx -= 1
|
|
37
|
+
if g.key_held("s"):
|
|
38
|
+
dy += 1
|
|
39
|
+
if g.key_held("w"):
|
|
40
|
+
dy -= 1
|
|
41
|
+
player.x = utils.clamp(player.x + dx * speed * dt, 18, width - 18)
|
|
42
|
+
player.y = utils.clamp(player.y + dy * speed * dt, 18, height - 18)
|
|
43
|
+
for c in list(coins):
|
|
44
|
+
if player.collides_with(c):
|
|
45
|
+
coins.remove(c)
|
|
46
|
+
score += 1
|
|
47
|
+
spawn_coin()
|
|
48
|
+
|
|
49
|
+
def draw(g):
|
|
50
|
+
g.clear("dark")
|
|
51
|
+
for c in coins:
|
|
52
|
+
c.draw(g)
|
|
53
|
+
player.draw(g)
|
|
54
|
+
g.text(f"得分 {score}", 16, 14, size=26, color="white")
|
|
55
|
+
g.text("方向键 / WASD 移动 · ESC 退出", 16, height - 34,
|
|
56
|
+
size=16, color=(150, 150, 160))
|
|
57
|
+
|
|
58
|
+
pc.run(update=update, draw=draw, width=width, height=height,
|
|
59
|
+
title="pycoode demo")
|