gamekit2d 0.3.2__tar.gz → 0.3.3__tar.gz

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.
Files changed (55) hide show
  1. {gamekit2d-0.3.2/gamekit2d.egg-info → gamekit2d-0.3.3}/PKG-INFO +2 -2
  2. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/README.md +1 -1
  3. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/__init__.py +1 -1
  4. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/core/game.py +94 -29
  5. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/render/backend.py +17 -15
  6. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/render/window.py +41 -35
  7. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/sprites/sprite.py +4 -1
  8. {gamekit2d-0.3.2 → gamekit2d-0.3.3/gamekit2d.egg-info}/PKG-INFO +2 -2
  9. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/pyproject.toml +1 -1
  10. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/LICENSE +0 -0
  11. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/MANIFEST.in +0 -0
  12. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/TUTORIAL.html +0 -0
  13. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/TUTORIAL.md +0 -0
  14. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/TUTORIAL_zh.md +0 -0
  15. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/examples/01_hello.py +0 -0
  16. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/examples/02_pong.py +0 -0
  17. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/examples/03_breakout.py +0 -0
  18. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/examples/04_sprite_image.py +0 -0
  19. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/examples/05_space_shooter.py +0 -0
  20. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/examples/assets/coin1.png +0 -0
  21. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/examples/assets/coin2.png +0 -0
  22. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/examples/assets/player.png +0 -0
  23. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/examples/assets/star.png +0 -0
  24. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/examples/make_assets.py +0 -0
  25. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/audio/__init__.py +0 -0
  26. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/audio/sound.py +0 -0
  27. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/core/__init__.py +0 -0
  28. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/core/keys.py +0 -0
  29. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/core/scene.py +0 -0
  30. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/fx/__init__.py +0 -0
  31. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/fx/draw.py +0 -0
  32. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/fx/particles.py +0 -0
  33. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/physics/__init__.py +0 -0
  34. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/physics/collision.py +0 -0
  35. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/render/__init__.py +0 -0
  36. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/render/surface.py +0 -0
  37. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/render/win32.py +0 -0
  38. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/sprites/__init__.py +0 -0
  39. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/sprites/animation.py +0 -0
  40. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/sprites/group.py +0 -0
  41. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/sprites/image.py +0 -0
  42. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/ui/__init__.py +0 -0
  43. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/ui/text.py +0 -0
  44. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/ui/widgets.py +0 -0
  45. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/utils/__init__.py +0 -0
  46. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/utils/color.py +0 -0
  47. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit/utils/vector.py +0 -0
  48. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit2d.egg-info/SOURCES.txt +0 -0
  49. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit2d.egg-info/dependency_links.txt +0 -0
  50. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/gamekit2d.egg-info/top_level.txt +0 -0
  51. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/setup.cfg +0 -0
  52. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/tests/run_selftest.py +0 -0
  53. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/tests/test_collision.py +0 -0
  54. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/tests/test_draw.py +0 -0
  55. {gamekit2d-0.3.2 → gamekit2d-0.3.3}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gamekit2d
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: A tiny zero-dependency 2D game library (standard library only, self-made GDI renderer + tkinter + winsound). A simpler pygame.
5
5
  Author: gamekit contributors
6
6
  License-Expression: MIT
@@ -82,7 +82,7 @@ def wave(dt):
82
82
 
83
83
  `Group` 是一组精灵的轻量容器:整组移动 / 隐藏 / 删除,不用自己写循环。精灵仍由 Game 统一渲染,Group 只做批量管理。
84
84
 
85
- `Game(..., dirty=True)` 开启**脏矩形局部更新**:不整屏清屏,只把上一帧真正移动过的精灵旧位置刷回背景色。实测:在"少量动态对象 + 静态背景"场景(菜单、UI、几个移动元素)与整屏清屏基本持平;**大量精灵同时移动时建议保持 `dirty=False`**(局部恢复比整屏 `memmove` 清屏更贵)。仅 GDI 后端有效。
85
+ `Game(..., dirty=True)` 开启**脏矩形局部更新**:不整屏清屏,只把上一帧真正移动过的精灵旧位置刷回背景色。配合 `game.sprite(..., static=True)` 标记静态元素(墙壁、砖块、UI 装饰)——它们只在第一帧画进后台缓冲,之后只要没被移动精灵压过就不再重画。实测"400 静态 + 8 动态"场景比每帧全画快 **1.46 倍**。大量精灵同时移动时仍用默认 `dirty=False`。仅 GDI 后端有效。
86
86
 
87
87
  ---
88
88
 
@@ -58,7 +58,7 @@ def wave(dt):
58
58
 
59
59
  `Group` 是一组精灵的轻量容器:整组移动 / 隐藏 / 删除,不用自己写循环。精灵仍由 Game 统一渲染,Group 只做批量管理。
60
60
 
61
- `Game(..., dirty=True)` 开启**脏矩形局部更新**:不整屏清屏,只把上一帧真正移动过的精灵旧位置刷回背景色。实测:在"少量动态对象 + 静态背景"场景(菜单、UI、几个移动元素)与整屏清屏基本持平;**大量精灵同时移动时建议保持 `dirty=False`**(局部恢复比整屏 `memmove` 清屏更贵)。仅 GDI 后端有效。
61
+ `Game(..., dirty=True)` 开启**脏矩形局部更新**:不整屏清屏,只把上一帧真正移动过的精灵旧位置刷回背景色。配合 `game.sprite(..., static=True)` 标记静态元素(墙壁、砖块、UI 装饰)——它们只在第一帧画进后台缓冲,之后只要没被移动精灵压过就不再重画。实测"400 静态 + 8 动态"场景比每帧全画快 **1.46 倍**。大量精灵同时移动时仍用默认 `dirty=False`。仅 GDI 后端有效。
62
62
 
63
63
  ---
64
64
 
@@ -46,7 +46,7 @@ from .utils.color import (Color, to_color, mix, random_color,
46
46
  LIGHT_GRAY, DARK_GRAY, NAVY)
47
47
  from .utils.vector import Vec2
48
48
 
49
- __version__ = "0.3.2"
49
+ __version__ = "0.3.3"
50
50
  __all__ = [
51
51
  "Game", "Scene", "Key", "normalize",
52
52
  "Sprite", "Group", "Animation",
@@ -63,6 +63,7 @@ class Game:
63
63
  self._dirty = bool(dirty) and (self.backend == "GdiRenderer")
64
64
  self._last_bboxes = {}
65
65
  self._moved = {}
66
+ self._dirty_first = True # 第一帧必须全画(静态精灵首次入缓冲)
66
67
 
67
68
  # ---- 全局物理 ----
68
69
  self.gravity = 0.0 # 像素/秒^2,作用于 gravity_scale>0 的精灵
@@ -188,15 +189,27 @@ class Game:
188
189
  r = self._renderer
189
190
 
190
191
  if self._dirty:
191
- # 脏矩形模式:不清整屏,只擦上一帧真正移动过的精灵旧位置
192
- r.begin_frame(clear=False)
193
- if self._moved:
194
- r.recover_background(list(self._moved.values()))
195
- self._moved = {}
192
+ self._render_dirty(r)
196
193
  else:
197
194
  r.begin_frame(clear=True)
195
+ self._draw_all(r)
198
196
 
199
- # 精灵与图形按 layer 混排绘制(layer 越大越靠上层)
197
+ for t in list(self._texts):
198
+ if not t._removed:
199
+ t._draw(r)
200
+
201
+ for u in list(self._ui_elements):
202
+ if not u._removed:
203
+ u._draw(r)
204
+
205
+ for ps in list(self._particles):
206
+ if not ps._removed:
207
+ ps._draw(r)
208
+
209
+ r.end_frame()
210
+
211
+ def _collect_drawables(self):
212
+ """收集当前帧需要绘制的精灵/图形,按 layer 排序。"""
200
213
  drawables = []
201
214
  for s in self._sprites:
202
215
  if not s._removed and s.visible:
@@ -204,36 +217,84 @@ class Game:
204
217
  for sh in self._shapes:
205
218
  if not sh._removed and sh.visible:
206
219
  drawables.append((sh.layer, sh))
220
+ drawables.sort(key=lambda x: x[0])
221
+ return drawables
222
+
223
+ def _draw_all(self, r):
224
+ """整屏清屏模式:画全部精灵/图形。"""
225
+ for _, obj in self._collect_drawables():
226
+ obj._draw(r)
227
+
228
+ @staticmethod
229
+ def _bbox_intersects(l, t, r, b, rect):
230
+ rl, rt, rr, rb = rect
231
+ return not (r < rl or l > rr or b < rt or t > rb)
232
+
233
+ def _render_dirty(self, r):
234
+ """脏矩形模式:不清整屏,静态精灵只画一次,动态精灵旧位置用背景恢复。"""
235
+ r.begin_frame(clear=False)
236
+ dirty = list(self._moved.values()) # 上一帧动态精灵的旧位置
237
+ if dirty:
238
+ r.recover_background(dirty)
239
+
240
+ statics = []
241
+ dynamics = []
207
242
  seen = set()
208
- for _, obj in sorted(drawables, key=lambda x: x[0]):
243
+ for s in self._sprites:
244
+ if s._removed or not s.visible:
245
+ continue
246
+ if getattr(s, "static", False):
247
+ statics.append((s.layer, s))
248
+ else:
249
+ dynamics.append((s.layer, s))
250
+
251
+ if self._dirty_first:
252
+ # 第一帧:所有精灵都要画进后台缓冲(包括静态的)
253
+ to_draw = [(sh.layer, sh) for sh in self._shapes if not sh._removed and sh.visible]
254
+ to_draw.extend(statics)
255
+ to_draw.extend(dynamics)
256
+ to_draw.sort(key=lambda x: x[0])
257
+ for _, obj in to_draw:
258
+ obj._draw(r)
259
+ if hasattr(obj, "left"):
260
+ seen.add(id(obj))
261
+ self._dirty_first = False
262
+ # 记录所有动态精灵 bbox(静态精灵不进 _last_bboxes,
263
+ # 它们位置不变,以后靠"与脏区相交才重画"管理)
264
+ for _, s in dynamics:
265
+ self._last_bboxes[id(s)] = (int(s.left), int(s.top),
266
+ int(s.right), int(s.bottom))
267
+ return
268
+
269
+ # 后续帧:静态精灵只有 bbox 与脏区相交时才重画(被动态精灵压过)
270
+ redraw_statics = []
271
+ for layer, s in statics:
272
+ sl = int(s.left); st = int(s.top); sr = int(s.right); sb = int(s.bottom)
273
+ if any(self._bbox_intersects(sl, st, sr, sb, d) for d in dirty):
274
+ redraw_statics.append((layer, s))
275
+
276
+ to_draw = redraw_statics + dynamics
277
+ # shapes 每帧重画(简单起见,不做静态优化)
278
+ for sh in self._shapes:
279
+ if not sh._removed and sh.visible:
280
+ to_draw.append((sh.layer, sh))
281
+ to_draw.sort(key=lambda x: x[0])
282
+
283
+ self._moved = {}
284
+ for _, obj in to_draw:
209
285
  obj._draw(r)
210
- if self._dirty and hasattr(obj, "left"):
286
+ if hasattr(obj, "left") and not getattr(obj, "static", False):
211
287
  bid = id(obj)
212
288
  seen.add(bid)
213
289
  nb = (int(obj.left), int(obj.top),
214
290
  int(obj.right), int(obj.bottom))
215
291
  old = self._last_bboxes.get(bid)
216
292
  if old is not None and old != nb:
217
- self._moved[bid] = old # 下一帧开头擦旧位置
293
+ self._moved[bid] = old
218
294
  self._last_bboxes[bid] = nb
219
- # 清理已删除精灵的 bbox 记录(避免字典无限增长)
220
- if self._dirty:
221
- for bid in [b for b in self._last_bboxes if b not in seen]:
222
- self._last_bboxes.pop(bid, None)
223
-
224
- for t in list(self._texts):
225
- if not t._removed:
226
- t._draw(r)
227
-
228
- for u in list(self._ui_elements):
229
- if not u._removed:
230
- u._draw(r)
231
-
232
- for ps in list(self._particles):
233
- if not ps._removed:
234
- ps._draw(r)
235
-
236
- r.end_frame()
295
+ # 清理已删除动态精灵的 bbox 记录
296
+ for bid in [b for b in self._last_bboxes if b not in seen]:
297
+ self._last_bboxes.pop(bid, None)
237
298
 
238
299
  # ==================================================================
239
300
  # 定时器
@@ -269,15 +330,19 @@ class Game:
269
330
  # 对象工厂
270
331
  # ==================================================================
271
332
  def sprite(self, image=None, x=0, y=0, width=None, height=None,
272
- color=None, shape="rect", tag=None, layer=0):
333
+ color=None, shape="rect", tag=None, layer=0, static=False):
273
334
  """创建并返回一个精灵。
274
335
 
275
336
  ``game.sprite("player.png", x=100, y=100)``
276
337
  ``game.sprite(color="red", x=200, y=200, width=40, height=40, shape="circle")``
338
+
339
+ ``static=True`` 标记静态精灵:dirty 模式下只在第一帧绘制,
340
+ 之后不再重画(被移动精灵压过时会自动重画那一块)。
277
341
  """
278
342
  from ..sprites.sprite import Sprite
279
343
  s = Sprite(self, image=image, x=x, y=y, width=width, height=height,
280
- color=color, shape=shape, tag=tag, layer=layer)
344
+ color=color, shape=shape, tag=tag, layer=layer,
345
+ static=static)
281
346
  if s not in self._sprites:
282
347
  self._sprites.append(s)
283
348
  return s
@@ -327,6 +327,9 @@ def _vk_to_keysym(vk):
327
327
  # 矩形 Surface 缓存上限:超过后回退直接 GDI 绘制,防 GDI 对象耗尽
328
328
  _RECT_CACHE_MAX = 256
329
329
 
330
+ # 进程级共享的隐藏 tk root:PhotoImage 解码需要 Tk 默认 root
331
+ _shared_decode_root = None
332
+
330
333
 
331
334
  class GdiRenderer(Renderer):
332
335
  """基于自研 GDI 内核的后端(立即模式)。
@@ -348,15 +351,18 @@ class GdiRenderer(Renderer):
348
351
  self._fonts = {} # (family, size, bold, italic) -> HFONT
349
352
  self._image_cache = {} # id(photo) -> Surface(图像精灵像素缓存)
350
353
  self._rect_cache = {} # (w,h,color[,outline]) -> Surface(矩形精灵缓存)
351
- self._decode_root = None # 隐藏 tk root,仅用于 PhotoImage 解码
352
- # 立即创建隐藏 root:Sprite 的 load_image 在 Game.__init__ 之后、
353
- # run 之前执行,需要默认 root 才能解码 PhotoImage。
354
- try:
355
- import tkinter as tk
356
- self._decode_root = tk.Tk()
357
- self._decode_root.withdraw()
358
- except Exception:
359
- self._decode_root = None
354
+ # 隐藏 tk root 是进程级共享单例:PhotoImage 解码需要 Tk 默认 root,
355
+ # 但 Tcl 一次只能有一个解释器。多个 Game 复用同一个 root,
356
+ # 不在每个后端关闭时 destroy(进程退出时一起回收)。
357
+ global _shared_decode_root
358
+ if _shared_decode_root is None:
359
+ try:
360
+ import tkinter as tk
361
+ _shared_decode_root = tk.Tk()
362
+ _shared_decode_root.withdraw()
363
+ except Exception:
364
+ _shared_decode_root = None
365
+ self._decode_root = _shared_decode_root
360
366
 
361
367
  # 输入状态
362
368
  self._keys_held = set()
@@ -401,12 +407,8 @@ class GdiRenderer(Renderer):
401
407
  pass
402
408
  self._rect_cache.clear()
403
409
  self._window.close()
404
- if self._decode_root is not None:
405
- try:
406
- self._decode_root.destroy()
407
- except Exception:
408
- pass
409
- self._decode_root = None
410
+ # decode_root 是进程级共享单例,不在此处 destroy
411
+ self._decode_root = None
410
412
 
411
413
  # ---- 每帧 ----
412
414
  def pump(self):
@@ -19,6 +19,32 @@ _CLASS_NAME = "GameKitRenderWindow"
19
19
 
20
20
  # 所有活着的 Window:hwnd -> Window,供 wndproc 回调(滚轮等)反查实例
21
21
  _WINDOWS = {}
22
+ # 窗口类只注册一次(重复 RegisterClassW 会让类句柄和 WNDPROC 闭包累积)
23
+ _class_registered = False
24
+
25
+
26
+ @win32.WNDPROC
27
+ def _wndproc(hwnd, msg, wparam, lparam):
28
+ """模块级窗口过程:不闭包,所有 GameKit 窗口共享。"""
29
+ if msg == WM_MOUSEWHEEL:
30
+ win = _WINDOWS.get(hwnd)
31
+ if win is not None:
32
+ delta = (wparam >> 16) & 0xFFFF
33
+ if delta >= 0x8000:
34
+ delta -= 0x10000
35
+ win._wheel_delta += delta
36
+ return 0
37
+ if msg == WM_CLOSE:
38
+ win32.user32.DestroyWindow(hwnd)
39
+ return 0
40
+ if msg == WM_DESTROY:
41
+ # 注意:这里不要 PostQuitMessage(0)。
42
+ # PostQuitMessage 会往线程消息队列塞 WM_QUIT,
43
+ # 若同进程随后再用 tkinter(Tcl 事件循环),残留的
44
+ # WM_QUIT 会让其悄悄退出。关闭检测改由 poll_events 的
45
+ # IsWindow 完成。
46
+ return 0
47
+ return win32.user32.DefWindowProcW(hwnd, msg, wparam, lparam)
22
48
 
23
49
 
24
50
  class Window:
@@ -51,41 +77,21 @@ class Window:
51
77
  # ------------------------------------------------------------ 创建
52
78
 
53
79
  def _register_class(self):
54
- @win32.WNDPROC
55
- def wndproc(hwnd, msg, wparam, lparam):
56
- if msg == WM_MOUSEWHEEL:
57
- win = _WINDOWS.get(hwnd)
58
- if win is not None:
59
- delta = (wparam >> 16) & 0xFFFF
60
- if delta >= 0x8000:
61
- delta -= 0x10000
62
- win._wheel_delta += delta
63
- return 0
64
- if msg == WM_CLOSE:
65
- win32.user32.DestroyWindow(hwnd)
66
- return 0
67
- if msg == WM_DESTROY:
68
- # 注意:这里不要 PostQuitMessage(0)。
69
- # PostQuitMessage 会往线程消息队列塞 WM_QUIT,
70
- # 若同进程随后再用 tkinter(Tcl 事件循环),残留的
71
- # WM_QUIT 会让其悄悄退出。关闭检测改由 poll_events 的
72
- # IsWindow 完成。
73
- return 0
74
- return win32.user32.DefWindowProcW(hwnd, msg, wparam, lparam)
75
-
76
- self._wndproc = wndproc # 保持引用
77
- wc = WNDCLASSW()
78
- wc.style = 0
79
- wc.lpfnWndProc = wndproc
80
- wc.cbClsExtra = 0
81
- wc.cbWndExtra = 0
82
- wc.hInstance = win32.hinstance()
83
- wc.hIcon = None
84
- wc.hCursor = None
85
- wc.hbrBackground = win32.gdi32.GetStockObject(win32.COLOR_WINDOW)
86
- wc.lpszMenuName = None
87
- wc.lpszClassName = _CLASS_NAME
88
- win32.user32.RegisterClassW(byref(wc))
80
+ global _class_registered
81
+ if not _class_registered:
82
+ wc = WNDCLASSW()
83
+ wc.style = 0
84
+ wc.lpfnWndProc = _wndproc
85
+ wc.cbClsExtra = 0
86
+ wc.cbWndExtra = 0
87
+ wc.hInstance = win32.hinstance()
88
+ wc.hIcon = None
89
+ wc.hCursor = None
90
+ wc.hbrBackground = win32.gdi32.GetStockObject(win32.COLOR_WINDOW)
91
+ wc.lpszMenuName = None
92
+ wc.lpszClassName = _CLASS_NAME
93
+ if win32.user32.RegisterClassW(byref(wc)):
94
+ _class_registered = True
89
95
 
90
96
  def _create_window(self, title):
91
97
  style = win32.WS_OVERLAPPEDWINDOW | win32.WS_VISIBLE
@@ -23,7 +23,7 @@ class Sprite:
23
23
  """
24
24
 
25
25
  def __init__(self, game, image=None, x=0, y=0, width=None, height=None,
26
- color=None, shape="rect", tag=None, layer=0):
26
+ color=None, shape="rect", tag=None, layer=0, static=False):
27
27
  self.game = game
28
28
 
29
29
  # ---- 位置与速度 ----
@@ -40,6 +40,9 @@ class Sprite:
40
40
  self.shape = shape if shape in ("rect", "circle") else "rect"
41
41
  self.layer = layer
42
42
  self.tag = tag
43
+ # static=True:标记为静态精灵。dirty 模式下它只在第一帧绘制,
44
+ # 之后只要没被动态精灵压过就不再重画(像素留在后台缓冲里)。
45
+ self.static = bool(static)
43
46
  self._image_path = None
44
47
  self._photo = None
45
48
  self._flip_h = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gamekit2d
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: A tiny zero-dependency 2D game library (standard library only, self-made GDI renderer + tkinter + winsound). A simpler pygame.
5
5
  Author: gamekit contributors
6
6
  License-Expression: MIT
@@ -82,7 +82,7 @@ def wave(dt):
82
82
 
83
83
  `Group` 是一组精灵的轻量容器:整组移动 / 隐藏 / 删除,不用自己写循环。精灵仍由 Game 统一渲染,Group 只做批量管理。
84
84
 
85
- `Game(..., dirty=True)` 开启**脏矩形局部更新**:不整屏清屏,只把上一帧真正移动过的精灵旧位置刷回背景色。实测:在"少量动态对象 + 静态背景"场景(菜单、UI、几个移动元素)与整屏清屏基本持平;**大量精灵同时移动时建议保持 `dirty=False`**(局部恢复比整屏 `memmove` 清屏更贵)。仅 GDI 后端有效。
85
+ `Game(..., dirty=True)` 开启**脏矩形局部更新**:不整屏清屏,只把上一帧真正移动过的精灵旧位置刷回背景色。配合 `game.sprite(..., static=True)` 标记静态元素(墙壁、砖块、UI 装饰)——它们只在第一帧画进后台缓冲,之后只要没被移动精灵压过就不再重画。实测"400 静态 + 8 动态"场景比每帧全画快 **1.46 倍**。大量精灵同时移动时仍用默认 `dirty=False`。仅 GDI 后端有效。
86
86
 
87
87
  ---
88
88
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gamekit2d"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "A tiny zero-dependency 2D game library (standard library only, self-made GDI renderer + tkinter + winsound). A simpler pygame."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes