diff --git a/translated_images/1.b6da8c1394b07491.hk.png b/translated_images/1.b6da8c1394b07491.hk.png new file mode 100644 index 000000000..4108840b9 Binary files /dev/null and b/translated_images/1.b6da8c1394b07491.hk.png differ diff --git a/translated_images/1.b6da8c1394b07491.ja.png b/translated_images/1.b6da8c1394b07491.ja.png new file mode 100644 index 000000000..630dcc959 Binary files /dev/null and b/translated_images/1.b6da8c1394b07491.ja.png differ diff --git a/translated_images/1.b6da8c1394b07491.mo.png b/translated_images/1.b6da8c1394b07491.mo.png new file mode 100644 index 000000000..19907c3dc Binary files /dev/null and b/translated_images/1.b6da8c1394b07491.mo.png differ diff --git a/translated_images/1.b6da8c1394b07491.tw.png b/translated_images/1.b6da8c1394b07491.tw.png new file mode 100644 index 000000000..f97ddaeee Binary files /dev/null and b/translated_images/1.b6da8c1394b07491.tw.png differ diff --git a/translated_images/1.b6da8c1394b07491.ur.png b/translated_images/1.b6da8c1394b07491.ur.png new file mode 100644 index 000000000..8e27f9e0c Binary files /dev/null and b/translated_images/1.b6da8c1394b07491.ur.png differ diff --git a/translated_images/1.b6da8c1394b07491.zh.png b/translated_images/1.b6da8c1394b07491.zh.png new file mode 100644 index 000000000..4ca84ef40 Binary files /dev/null and b/translated_images/1.b6da8c1394b07491.zh.png differ diff --git a/translated_images/1.cc07a5cbe114ad1d.hk.png b/translated_images/1.cc07a5cbe114ad1d.hk.png new file mode 100644 index 000000000..b568c7856 Binary files /dev/null and b/translated_images/1.cc07a5cbe114ad1d.hk.png differ diff --git a/translated_images/1.cc07a5cbe114ad1d.ja.png b/translated_images/1.cc07a5cbe114ad1d.ja.png new file mode 100644 index 000000000..36f6cd46e Binary files /dev/null and b/translated_images/1.cc07a5cbe114ad1d.ja.png differ diff --git a/translated_images/1.cc07a5cbe114ad1d.mo.png b/translated_images/1.cc07a5cbe114ad1d.mo.png new file mode 100644 index 000000000..b568c7856 Binary files /dev/null and b/translated_images/1.cc07a5cbe114ad1d.mo.png differ diff --git a/translated_images/1.cc07a5cbe114ad1d.tw.png b/translated_images/1.cc07a5cbe114ad1d.tw.png new file mode 100644 index 000000000..15c011e0b Binary files /dev/null and b/translated_images/1.cc07a5cbe114ad1d.tw.png differ diff --git a/translated_images/1.cc07a5cbe114ad1d.ur.png b/translated_images/1.cc07a5cbe114ad1d.ur.png new file mode 100644 index 000000000..75eb82065 Binary files /dev/null and b/translated_images/1.cc07a5cbe114ad1d.ur.png differ diff --git a/translated_images/1.cc07a5cbe114ad1d.zh.png b/translated_images/1.cc07a5cbe114ad1d.zh.png new file mode 100644 index 000000000..396fbb798 Binary files /dev/null and b/translated_images/1.cc07a5cbe114ad1d.zh.png differ diff --git a/translated_images/2.1dae52ff08042246.hk.png b/translated_images/2.1dae52ff08042246.hk.png new file mode 100644 index 000000000..0b760500e Binary files /dev/null and b/translated_images/2.1dae52ff08042246.hk.png differ diff --git a/translated_images/2.1dae52ff08042246.ja.png b/translated_images/2.1dae52ff08042246.ja.png new file mode 100644 index 000000000..e4881a12d Binary files /dev/null and b/translated_images/2.1dae52ff08042246.ja.png differ diff --git a/translated_images/2.1dae52ff08042246.mo.png b/translated_images/2.1dae52ff08042246.mo.png new file mode 100644 index 000000000..9c902483e Binary files /dev/null and b/translated_images/2.1dae52ff08042246.mo.png differ diff --git a/translated_images/2.1dae52ff08042246.tw.png b/translated_images/2.1dae52ff08042246.tw.png new file mode 100644 index 000000000..ea8ecb1d9 Binary files /dev/null and b/translated_images/2.1dae52ff08042246.tw.png differ diff --git a/translated_images/2.1dae52ff08042246.ur.png b/translated_images/2.1dae52ff08042246.ur.png new file mode 100644 index 000000000..5bd882fdd Binary files /dev/null and b/translated_images/2.1dae52ff08042246.ur.png differ diff --git a/translated_images/2.1dae52ff08042246.zh.png b/translated_images/2.1dae52ff08042246.zh.png new file mode 100644 index 000000000..206e2226a Binary files /dev/null and b/translated_images/2.1dae52ff08042246.zh.png differ diff --git a/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.hk.png b/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.hk.png new file mode 100644 index 000000000..c088c51f2 Binary files /dev/null and b/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.hk.png differ diff --git a/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.ja.png b/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.ja.png new file mode 100644 index 000000000..33df18e5c Binary files /dev/null and b/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.ja.png differ diff --git a/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.mo.png b/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.mo.png new file mode 100644 index 000000000..063cc78c6 Binary files /dev/null and b/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.mo.png differ diff --git a/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.tw.png b/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.tw.png new file mode 100644 index 000000000..316f5aa20 Binary files /dev/null and b/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.tw.png differ diff --git a/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.ur.png b/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.ur.png new file mode 100644 index 000000000..739902325 Binary files /dev/null and b/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.ur.png differ diff --git a/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.zh.png b/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.zh.png new file mode 100644 index 000000000..2194fc030 Binary files /dev/null and b/translated_images/after-codeswing-extension-pb.0ebddddcf73b5509.zh.png differ diff --git a/translated_images/background.148a8d43afde5730.hk.png b/translated_images/background.148a8d43afde5730.hk.png new file mode 100644 index 000000000..3a0e6b10b Binary files /dev/null and b/translated_images/background.148a8d43afde5730.hk.png differ diff --git a/translated_images/background.148a8d43afde5730.ja.png b/translated_images/background.148a8d43afde5730.ja.png new file mode 100644 index 000000000..627924c32 Binary files /dev/null and b/translated_images/background.148a8d43afde5730.ja.png differ diff --git a/translated_images/background.148a8d43afde5730.mo.png b/translated_images/background.148a8d43afde5730.mo.png new file mode 100644 index 000000000..cd5e7b544 Binary files /dev/null and b/translated_images/background.148a8d43afde5730.mo.png differ diff --git a/translated_images/background.148a8d43afde5730.tw.png b/translated_images/background.148a8d43afde5730.tw.png new file mode 100644 index 000000000..3c386e90d Binary files /dev/null and b/translated_images/background.148a8d43afde5730.tw.png differ diff --git a/translated_images/background.148a8d43afde5730.ur.png b/translated_images/background.148a8d43afde5730.ur.png new file mode 100644 index 000000000..8a2bb7857 Binary files /dev/null and b/translated_images/background.148a8d43afde5730.ur.png differ diff --git a/translated_images/background.148a8d43afde5730.zh.png b/translated_images/background.148a8d43afde5730.zh.png new file mode 100644 index 000000000..fc1db82d6 Binary files /dev/null and b/translated_images/background.148a8d43afde5730.zh.png differ diff --git a/translated_images/backgroundColor.e19c3c60768150c8.hk.png b/translated_images/backgroundColor.e19c3c60768150c8.hk.png new file mode 100644 index 000000000..3bd93a532 Binary files /dev/null and b/translated_images/backgroundColor.e19c3c60768150c8.hk.png differ diff --git a/translated_images/backgroundColor.e19c3c60768150c8.ja.png b/translated_images/backgroundColor.e19c3c60768150c8.ja.png new file mode 100644 index 000000000..3bd93a532 Binary files /dev/null and b/translated_images/backgroundColor.e19c3c60768150c8.ja.png differ diff --git a/translated_images/backgroundColor.e19c3c60768150c8.mo.png b/translated_images/backgroundColor.e19c3c60768150c8.mo.png new file mode 100644 index 000000000..3bd93a532 Binary files /dev/null and b/translated_images/backgroundColor.e19c3c60768150c8.mo.png differ diff --git a/translated_images/backgroundColor.e19c3c60768150c8.tw.png b/translated_images/backgroundColor.e19c3c60768150c8.tw.png new file mode 100644 index 000000000..3bd93a532 Binary files /dev/null and b/translated_images/backgroundColor.e19c3c60768150c8.tw.png differ diff --git a/translated_images/backgroundColor.e19c3c60768150c8.ur.png b/translated_images/backgroundColor.e19c3c60768150c8.ur.png new file mode 100644 index 000000000..3bd93a532 Binary files /dev/null and b/translated_images/backgroundColor.e19c3c60768150c8.ur.png differ diff --git a/translated_images/backgroundColor.e19c3c60768150c8.zh.png b/translated_images/backgroundColor.e19c3c60768150c8.zh.png new file mode 100644 index 000000000..3bd93a532 Binary files /dev/null and b/translated_images/backgroundColor.e19c3c60768150c8.zh.png differ diff --git a/translated_images/browser-console.efaf0b51aaaf6778.hk.png b/translated_images/browser-console.efaf0b51aaaf6778.hk.png new file mode 100644 index 000000000..00644e295 Binary files /dev/null and b/translated_images/browser-console.efaf0b51aaaf6778.hk.png differ diff --git a/translated_images/browser-console.efaf0b51aaaf6778.ja.png b/translated_images/browser-console.efaf0b51aaaf6778.ja.png new file mode 100644 index 000000000..7df1cd5b3 Binary files /dev/null and b/translated_images/browser-console.efaf0b51aaaf6778.ja.png differ diff --git a/translated_images/browser-console.efaf0b51aaaf6778.mo.png b/translated_images/browser-console.efaf0b51aaaf6778.mo.png new file mode 100644 index 000000000..eb71018d5 Binary files /dev/null and b/translated_images/browser-console.efaf0b51aaaf6778.mo.png differ diff --git a/translated_images/browser-console.efaf0b51aaaf6778.tw.png b/translated_images/browser-console.efaf0b51aaaf6778.tw.png new file mode 100644 index 000000000..eb71018d5 Binary files /dev/null and b/translated_images/browser-console.efaf0b51aaaf6778.tw.png differ diff --git a/translated_images/browser-console.efaf0b51aaaf6778.ur.png b/translated_images/browser-console.efaf0b51aaaf6778.ur.png new file mode 100644 index 000000000..c2d0e1f86 Binary files /dev/null and b/translated_images/browser-console.efaf0b51aaaf6778.ur.png differ diff --git a/translated_images/browser-console.efaf0b51aaaf6778.zh.png b/translated_images/browser-console.efaf0b51aaaf6778.zh.png new file mode 100644 index 000000000..b2afde336 Binary files /dev/null and b/translated_images/browser-console.efaf0b51aaaf6778.zh.png differ diff --git a/translated_images/browser.60317c9be8b7f84a.hk.jpg b/translated_images/browser.60317c9be8b7f84a.hk.jpg new file mode 100644 index 000000000..6fa968cc5 Binary files /dev/null and b/translated_images/browser.60317c9be8b7f84a.hk.jpg differ diff --git a/translated_images/browser.60317c9be8b7f84a.ja.jpg b/translated_images/browser.60317c9be8b7f84a.ja.jpg new file mode 100644 index 000000000..88c382d64 Binary files /dev/null and b/translated_images/browser.60317c9be8b7f84a.ja.jpg differ diff --git a/translated_images/browser.60317c9be8b7f84a.mo.jpg b/translated_images/browser.60317c9be8b7f84a.mo.jpg new file mode 100644 index 000000000..fac325fbe Binary files /dev/null and b/translated_images/browser.60317c9be8b7f84a.mo.jpg differ diff --git a/translated_images/browser.60317c9be8b7f84a.tw.jpg b/translated_images/browser.60317c9be8b7f84a.tw.jpg new file mode 100644 index 000000000..b09aa15ef Binary files /dev/null and b/translated_images/browser.60317c9be8b7f84a.tw.jpg differ diff --git a/translated_images/browser.60317c9be8b7f84a.ur.jpg b/translated_images/browser.60317c9be8b7f84a.ur.jpg new file mode 100644 index 000000000..f459b2c1e Binary files /dev/null and b/translated_images/browser.60317c9be8b7f84a.ur.jpg differ diff --git a/translated_images/browser.60317c9be8b7f84a.zh.jpg b/translated_images/browser.60317c9be8b7f84a.zh.jpg new file mode 100644 index 000000000..5da61c1c3 Binary files /dev/null and b/translated_images/browser.60317c9be8b7f84a.zh.jpg differ diff --git a/translated_images/canvas.fbd605ff8e5b8aff.hk.png b/translated_images/canvas.fbd605ff8e5b8aff.hk.png new file mode 100644 index 000000000..5c85de5ba Binary files /dev/null and b/translated_images/canvas.fbd605ff8e5b8aff.hk.png differ diff --git a/translated_images/canvas.fbd605ff8e5b8aff.ja.png b/translated_images/canvas.fbd605ff8e5b8aff.ja.png new file mode 100644 index 000000000..19a3de5df Binary files /dev/null and b/translated_images/canvas.fbd605ff8e5b8aff.ja.png differ diff --git a/translated_images/canvas.fbd605ff8e5b8aff.mo.png b/translated_images/canvas.fbd605ff8e5b8aff.mo.png new file mode 100644 index 000000000..97b75a531 Binary files /dev/null and b/translated_images/canvas.fbd605ff8e5b8aff.mo.png differ diff --git a/translated_images/canvas.fbd605ff8e5b8aff.tw.png b/translated_images/canvas.fbd605ff8e5b8aff.tw.png new file mode 100644 index 000000000..f92f855d4 Binary files /dev/null and b/translated_images/canvas.fbd605ff8e5b8aff.tw.png differ diff --git a/translated_images/canvas.fbd605ff8e5b8aff.ur.png b/translated_images/canvas.fbd605ff8e5b8aff.ur.png new file mode 100644 index 000000000..45d95d434 Binary files /dev/null and b/translated_images/canvas.fbd605ff8e5b8aff.ur.png differ diff --git a/translated_images/canvas.fbd605ff8e5b8aff.zh.png b/translated_images/canvas.fbd605ff8e5b8aff.zh.png new file mode 100644 index 000000000..5555862e7 Binary files /dev/null and b/translated_images/canvas.fbd605ff8e5b8aff.zh.png differ diff --git a/translated_images/canvas_grid.5f209da785ded492.hk.png b/translated_images/canvas_grid.5f209da785ded492.hk.png new file mode 100644 index 000000000..b875684c9 Binary files /dev/null and b/translated_images/canvas_grid.5f209da785ded492.hk.png differ diff --git a/translated_images/canvas_grid.5f209da785ded492.ja.png b/translated_images/canvas_grid.5f209da785ded492.ja.png new file mode 100644 index 000000000..acde8253c Binary files /dev/null and b/translated_images/canvas_grid.5f209da785ded492.ja.png differ diff --git a/translated_images/canvas_grid.5f209da785ded492.mo.png b/translated_images/canvas_grid.5f209da785ded492.mo.png new file mode 100644 index 000000000..043df6eac Binary files /dev/null and b/translated_images/canvas_grid.5f209da785ded492.mo.png differ diff --git a/translated_images/canvas_grid.5f209da785ded492.tw.png b/translated_images/canvas_grid.5f209da785ded492.tw.png new file mode 100644 index 000000000..b875684c9 Binary files /dev/null and b/translated_images/canvas_grid.5f209da785ded492.tw.png differ diff --git a/translated_images/canvas_grid.5f209da785ded492.ur.png b/translated_images/canvas_grid.5f209da785ded492.ur.png new file mode 100644 index 000000000..af78f33d9 Binary files /dev/null and b/translated_images/canvas_grid.5f209da785ded492.ur.png differ diff --git a/translated_images/canvas_grid.5f209da785ded492.zh.png b/translated_images/canvas_grid.5f209da785ded492.zh.png new file mode 100644 index 000000000..ebe4e3fd6 Binary files /dev/null and b/translated_images/canvas_grid.5f209da785ded492.zh.png differ diff --git a/translated_images/character.5c0dd8e067ffd693.hk.png b/translated_images/character.5c0dd8e067ffd693.hk.png new file mode 100644 index 000000000..708bed602 Binary files /dev/null and b/translated_images/character.5c0dd8e067ffd693.hk.png differ diff --git a/translated_images/character.5c0dd8e067ffd693.ja.png b/translated_images/character.5c0dd8e067ffd693.ja.png new file mode 100644 index 000000000..9324d6b3e Binary files /dev/null and b/translated_images/character.5c0dd8e067ffd693.ja.png differ diff --git a/translated_images/character.5c0dd8e067ffd693.mo.png b/translated_images/character.5c0dd8e067ffd693.mo.png new file mode 100644 index 000000000..29e9a1fe8 Binary files /dev/null and b/translated_images/character.5c0dd8e067ffd693.mo.png differ diff --git a/translated_images/character.5c0dd8e067ffd693.tw.png b/translated_images/character.5c0dd8e067ffd693.tw.png new file mode 100644 index 000000000..ac1a9e500 Binary files /dev/null and b/translated_images/character.5c0dd8e067ffd693.tw.png differ diff --git a/translated_images/character.5c0dd8e067ffd693.ur.png b/translated_images/character.5c0dd8e067ffd693.ur.png new file mode 100644 index 000000000..6da313755 Binary files /dev/null and b/translated_images/character.5c0dd8e067ffd693.ur.png differ diff --git a/translated_images/character.5c0dd8e067ffd693.zh.png b/translated_images/character.5c0dd8e067ffd693.zh.png new file mode 100644 index 000000000..4e6d15f7b Binary files /dev/null and b/translated_images/character.5c0dd8e067ffd693.zh.png differ diff --git a/translated_images/click-register.e89a30bf0d4bc9ca.hk.png b/translated_images/click-register.e89a30bf0d4bc9ca.hk.png new file mode 100644 index 000000000..ccc65305d Binary files /dev/null and b/translated_images/click-register.e89a30bf0d4bc9ca.hk.png differ diff --git a/translated_images/click-register.e89a30bf0d4bc9ca.ja.png b/translated_images/click-register.e89a30bf0d4bc9ca.ja.png new file mode 100644 index 000000000..ccc65305d Binary files /dev/null and b/translated_images/click-register.e89a30bf0d4bc9ca.ja.png differ diff --git a/translated_images/click-register.e89a30bf0d4bc9ca.mo.png b/translated_images/click-register.e89a30bf0d4bc9ca.mo.png new file mode 100644 index 000000000..ccc65305d Binary files /dev/null and b/translated_images/click-register.e89a30bf0d4bc9ca.mo.png differ diff --git a/translated_images/click-register.e89a30bf0d4bc9ca.tw.png b/translated_images/click-register.e89a30bf0d4bc9ca.tw.png new file mode 100644 index 000000000..ccc65305d Binary files /dev/null and b/translated_images/click-register.e89a30bf0d4bc9ca.tw.png differ diff --git a/translated_images/click-register.e89a30bf0d4bc9ca.ur.png b/translated_images/click-register.e89a30bf0d4bc9ca.ur.png new file mode 100644 index 000000000..ccc65305d Binary files /dev/null and b/translated_images/click-register.e89a30bf0d4bc9ca.ur.png differ diff --git a/translated_images/click-register.e89a30bf0d4bc9ca.zh.png b/translated_images/click-register.e89a30bf0d4bc9ca.zh.png new file mode 100644 index 000000000..ccc65305d Binary files /dev/null and b/translated_images/click-register.e89a30bf0d4bc9ca.zh.png differ diff --git a/translated_images/clone_repo.5085c48d666ead57.hk.png b/translated_images/clone_repo.5085c48d666ead57.hk.png new file mode 100644 index 000000000..6f7449fd8 Binary files /dev/null and b/translated_images/clone_repo.5085c48d666ead57.hk.png differ diff --git a/translated_images/clone_repo.5085c48d666ead57.ja.png b/translated_images/clone_repo.5085c48d666ead57.ja.png new file mode 100644 index 000000000..98e7d75e8 Binary files /dev/null and b/translated_images/clone_repo.5085c48d666ead57.ja.png differ diff --git a/translated_images/clone_repo.5085c48d666ead57.mo.png b/translated_images/clone_repo.5085c48d666ead57.mo.png new file mode 100644 index 000000000..dd839ffb6 Binary files /dev/null and b/translated_images/clone_repo.5085c48d666ead57.mo.png differ diff --git a/translated_images/clone_repo.5085c48d666ead57.tw.png b/translated_images/clone_repo.5085c48d666ead57.tw.png new file mode 100644 index 000000000..ef2b17f3f Binary files /dev/null and b/translated_images/clone_repo.5085c48d666ead57.tw.png differ diff --git a/translated_images/clone_repo.5085c48d666ead57.ur.png b/translated_images/clone_repo.5085c48d666ead57.ur.png new file mode 100644 index 000000000..8610d9547 Binary files /dev/null and b/translated_images/clone_repo.5085c48d666ead57.ur.png differ diff --git a/translated_images/clone_repo.5085c48d666ead57.zh.png b/translated_images/clone_repo.5085c48d666ead57.zh.png new file mode 100644 index 000000000..8a5d23e3d Binary files /dev/null and b/translated_images/clone_repo.5085c48d666ead57.zh.png differ diff --git a/translated_images/clone_repo.6a202fb230ab6bdd.hk.png b/translated_images/clone_repo.6a202fb230ab6bdd.hk.png new file mode 100644 index 000000000..e24724844 Binary files /dev/null and b/translated_images/clone_repo.6a202fb230ab6bdd.hk.png differ diff --git a/translations/zh/6-space-game/3-moving-elements-around/README.md b/translations/zh/6-space-game/3-moving-elements-around/README.md index 5a7055c94..1184f33bf 100644 --- a/translations/zh/6-space-game/3-moving-elements-around/README.md +++ b/translations/zh/6-space-game/3-moving-elements-around/README.md @@ -1,114 +1,283 @@ -# 构建太空游戏第三部分:添加运动 - +# 构建太空游戏 第3部分:添加运动 + +```mermaid +journey + title 你的游戏动画之旅 + section 运动基础 + 理解运动原理: 3: Student + 学习坐标更新: 4: Student + 实现基本移动: 4: Student + section 玩家控制 + 处理键盘事件: 4: Student + 阻止默认行为: 5: Student + 创建响应式控件: 5: Student + section 游戏系统 + 构建游戏循环: 5: Student + 管理对象生命周期: 5: Student + 实现发布/订阅模式: 5: Student +``` +想想你最喜欢的游戏——让它们吸引人不仅仅是漂亮的图形,而是所有东西如何移动并响应你的操作。现在,你的太空游戏就像一幅美丽的画,但我们马上要添加使其充满活力的动作。 + +当 NASA 工程师为阿波罗任务编写导航计算机程序时,他们面临着类似的挑战:如何让航天器在自动保持航向校正的同时响应飞行员的指令?我们今天要学习的原理正是这些——管理玩家控制的运动和自动系统行为协同工作。 + +本课中,你将学习如何让飞船在屏幕上滑行,响应玩家命令,创建流畅的移动模式。我们会将所有内容拆解成易于理解且自然递进的概念。 + +最后,你将拥有一个那样的游戏:玩家可以驾驶英雄飞船在屏幕上飞行,而敌舰在头顶巡逻。更重要的是,你将理解驱动游戏运动系统的核心原理。 + +```mermaid +mindmap + root((游戏动画)) + Movement Types + Player Controlled + Automatic Motion + Physics Based + Scripted Paths + Event Handling + Keyboard Input + Mouse Events + Touch Controls + Default Prevention + Game Loop + Update Logic + Render Frame + Clear Canvas + Frame Rate Control + Object Management + Position Updates + Collision Detection + Lifecycle Management + State Tracking + Communication + Pub/Sub Pattern + Event Emitters + Message Passing + Loose Coupling +``` ## 课前测验 [课前测验](https://ff-quizzes.netlify.app/web/quiz/33) -游戏只有在屏幕上有外星人跑来跑去时才会变得有趣!在这个游戏中,我们将使用两种类型的运动: +## 理解游戏运动 + +当游戏中的元素开始移动时,游戏才会真正生动起来,基本上有两种运动方式: -- **键盘/鼠标运动**:当用户通过键盘或鼠标与屏幕上的对象交互时。 -- **游戏驱动运动**:当游戏以一定的时间间隔移动对象时。 +- **玩家控制的运动**:当你按下一个键或点击鼠标时,某事物会移动。这是你与游戏世界之间的直接联系。 +- **自动运动**:游戏本身决定移动某些东西——比如那些无论你是否操作都在屏幕上巡逻的敌舰。 -那么我们如何在屏幕上移动物体呢?这完全是关于笛卡尔坐标系的:我们改变对象的位置 (x, y),然后重新绘制屏幕。 +在计算机屏幕上移动物体其实没你想的复杂。还记得数学课上的 x 和 y 坐标吗?这正是我们现在使用的。1610年,当伽利略追踪木星的卫星时,实际上也是在做同样事情——随着时间绘制位置以理解运动规律。 -通常,你需要以下步骤来实现屏幕上的*运动*: +屏幕上移动事物就像制作翻页动画——你需要遵循以下三个简单步骤: + +```mermaid +flowchart LR + A["第 N 帧"] --> B["更新位置"] + B --> C["清除画布"] + C --> D["绘制对象"] + D --> E["第 N+1 帧"] + E --> F{继续?} + F -->|是| B + F -->|否| G["游戏结束"] + + subgraph "动画循环" + H["1. 计算新位置"] + I["2. 擦除上一帧"] + J["3. 渲染新帧"] + end + + style B fill:#e1f5fe + style C fill:#ffebee + style D fill:#e8f5e8 +``` +1. **更新位置**——改变物体应该所在的位置(比如向右移动5像素) +2. **擦除旧帧**——清除屏幕防止出现鬼影尾迹 +3. **绘制新帧**——将物体放到新位置 -1. **设置新位置**:为对象设置一个新位置,这是让用户感知对象移动的必要步骤。 -2. **清除屏幕**:在每次绘制之间需要清除屏幕。我们可以通过绘制一个填充背景色的矩形来清除屏幕。 -3. **在新位置重新绘制对象**:通过这样做,我们最终实现了将对象从一个位置移动到另一个位置。 +做得够快,砰!你就拥有顺滑且自然的运动效果。 -以下是代码示例: +下面是代码中的表现: ```javascript -//set the hero's location +// 设置英雄的位置 hero.x += 5; -// clear the rectangle that hosts the hero +// 清除英雄所在的矩形区域 ctx.clearRect(0, 0, canvas.width, canvas.height); -// redraw the game background and hero -ctx.fillRect(0, 0, canvas.width, canvas.height) +// 重新绘制游戏背景和英雄 +ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; ctx.drawImage(heroImg, hero.x, hero.y); ``` -✅ 你能想到为什么每秒多次重绘你的英雄可能会导致性能问题吗?阅读关于[这种模式的替代方案](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas)。 +**这段代码的作用是:** +- **更新**英雄的 x 坐标,加5像素,实现水平移动 +- **清空**整个画布区域,移除上一帧 +- **填充**画布黑色背景 +- **重绘**英雄图片于新位置 + +✅ 你能想出为什么每秒多次重绘英雄可能导致性能开销吗?阅读关于[这种模式的替代方案](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas)。 ## 处理键盘事件 -通过将特定事件附加到代码上来处理事件。键盘事件会在整个窗口上触发,而鼠标事件(如`click`)可以连接到点击特定元素。我们将在整个项目中使用键盘事件。 +这里是我们将玩家输入连接到游戏动作的地方。当有人按下空格键开火激光或点击方向键躲避小行星时,你的游戏需要检测到并响应这些输入。 + +键盘事件发生在 window 级别,也就是说整个浏览器窗口都在监听键盘按下。鼠标点击可以绑定到特定元素(如按钮)。对于我们的太空游戏,我们专注键盘控制——这赋予玩家经典街机的感觉。 -要处理事件,你需要使用窗口的`addEventListener()`方法,并提供两个输入参数。第一个参数是事件的名称,例如`keyup`。第二个参数是事件发生时应该调用的函数。 +这让我想到1800年代电报操作员如何将莫尔斯电码翻译成有意义的信息——我们也正做类似事情,将按键转成游戏指令。 -以下是一个示例: +处理事件需要使用 window 的 `addEventListener()` 方法,传入两个参数。第一个是事件名,例如 `keyup`;第二个是事件触发时调用的函数。 + +示例: ```javascript window.addEventListener('keyup', (evt) => { - // `evt.key` = string representation of the key + // evt.key = 键的字符串表示 if (evt.key === 'ArrowUp') { - // do something + // 做某事 } -}) +}); ``` -对于键盘事件,事件上有两个属性可以用来查看按下了哪个键: +**这里发生了什么:** +- **监听**整个窗口的键盘事件 +- **捕获**事件对象,包含按了哪个键的信息 +- **检查**按键是否匹配特定键(这里是向上箭头) +- **满足条件时**执行相应代码 + +键盘事件可通过以下属性判断按了什么键: -- `key`:这是按下的键的字符串表示,例如`ArrowUp`。 -- `keyCode`:这是按下的键的数字表示,例如`37`,对应于`ArrowLeft`。 +- `key` —— 字符串形式,比如 `'ArrowUp'` +- `keyCode` —— 数字形式,比如 `37`,对应 `ArrowLeft` -✅ 键盘事件的操作在游戏开发之外也很有用。你能想到这种技术的其他用途吗? +✅ 键盘事件处理适用游戏开发外的场景,你还能想到哪些用途? + +```mermaid +sequenceDiagram + participant User + participant Browser + participant EventSystem + participant GameLogic + participant Hero + + User->>Browser: 按下 ArrowUp 键 + Browser->>EventSystem: keydown 事件 + EventSystem->>EventSystem: preventDefault() + EventSystem->>GameLogic: emit('KEY_EVENT_UP') + GameLogic->>Hero: hero.y -= 5 + Hero->>Hero: 更新位置 + + Note over Browser,GameLogic: 事件流阻止浏览器默认行为 + Note over GameLogic,Hero: 发布/订阅模式实现清晰通信 +``` +### 特殊键:须知! -### 特殊键:一个注意事项 +部分按键默认行为会影响游戏体验。箭头键会滚动页面,空格键会跳到页面下方——当玩家驾驶飞船时,这些行为当然不希望发生。 -有一些*特殊*键会影响窗口。这意味着如果你正在监听`keyup`事件,并使用这些特殊键移动你的英雄,它也会执行水平滚动。因此,在构建游戏时,你可能需要*关闭*这种内置的浏览器行为。你需要如下代码: +我们可以阻止这些默认行为,让游戏自行处理输入。这类似早期程序员覆盖系统中断实现自定义行为——我们这次是浏览器层面的操作。方法如下: ```javascript -let onKeyDown = function (e) { +const onKeyDown = function (e) { console.log(e.keyCode); switch (e.keyCode) { case 37: case 39: case 38: - case 40: // Arrow keys + case 40: // 方向键 case 32: e.preventDefault(); - break; // Space + break; // 空格键 default: - break; // do not block other keys + break; // 不阻塞其他按键 } }; window.addEventListener('keydown', onKeyDown); ``` -上述代码将确保箭头键和空格键的*默认*行为被关闭。*关闭*机制发生在调用`e.preventDefault()`时。 +**理解这段阻止代码:** +- **检查**可能引起问题的键码 +- **阻止**箭头键和空格键的默认浏览器行为 +- **允许**其他键正常工作 +- **使用** `e.preventDefault()` 阻止浏览器默认动作 + +### 🔄 **教学确认** +**事件处理理解点**:转入自动运动前,确保你能: +- ✅ 解释 `keydown` 和 `keyup` 事件的区别 +- ✅ 理解为何需要防止默认浏览器行为 +- ✅ 描述事件监听器如何连接用户输入与游戏逻辑 +- ✅ 识别哪些键可能会干扰游戏操作 + +**快速自测**:如果你不阻止箭头键的默认行为会怎样? +*答:浏览器会滚动页面,影响游戏运动* + +**事件系统架构**:你现在理解了: +- **窗口级监听**:在浏览器层捕获事件 +- **事件对象属性**:`key` 字符串与 `keyCode` 数字 +- **默认阻止**:防止不想要的浏览器行为 +- **条件逻辑**:针对特定按键做响应 -## 游戏驱动运动 +## 游戏引导的运动 -我们可以通过使用计时器(例如`setTimeout()`或`setInterval()`函数)让物体自行移动,这些计时器会在每个时间间隔更新对象的位置。以下是代码示例: +接下来聊聊无需玩家输入也会运动的物体。想想敌舰在屏幕上巡航,子弹直线飞行,背景云朵飘过。这种自主运动让游戏世界即使无人操作也充满生命力。 + +我们使用 JavaScript 内置计时器,定时更新位置。这就像钟摆时钟——定期触发动作。示例如下: ```javascript -let id = setInterval(() => { - //move the enemy on the y axis +const id = setInterval(() => { + // 在y轴上移动敌人 enemy.y += 10; -}) +}, 100); ``` +**这段运动代码的作用:** +- **创建**一个每100毫秒运行一次的定时器 +- **每次**更新敌人 y 坐标,下移10像素 +- **存储**定时器ID以备后续停止 +- **自动**将敌人往屏幕下方移动 + ## 游戏循环 -游戏循环是一个概念,实际上是一个以固定时间间隔调用的函数。它被称为游戏循环,因为所有应该显示给用户的内容都在循环中绘制。游戏循环利用游戏中的所有对象,绘制所有对象,除非某些对象不再属于游戏。例如,如果一个对象是被激光击中的敌人并爆炸,它就不再属于当前的游戏循环(你将在后续课程中了解更多)。 +这是贯穿全局的核心机制——游戏循环。把游戏比作电影,游戏循环就是放映机,快速播放一帧又一帧,营造顺畅运动感。 + +每款游戏背后都有一个这样的循环。它持续更新所有游戏对象,重绘屏幕,重复进行。追踪英雄、所有敌人及飞行的激光——保持整局游戏状态同步。 + +这让我想起早期动画师如华特·迪士尼,逐帧重绘角色制造运动假象。我们同理,不过用代码替代铅笔。 -以下是一个典型的游戏循环代码示例: +一般的游戏循环代码如下: +```mermaid +flowchart TD + A["开始游戏循环"] --> B["清除画布"] + B --> C["填充背景"] + C --> D["更新游戏对象"] + D --> E["绘制英雄"] + E --> F["绘制敌人"] + F --> G["绘制界面元素"] + G --> H["等待下一帧"] + H --> I{游戏运行中?} + I -->|是| B + I -->|否| J["结束游戏"] + + subgraph "帧率控制" + K["60帧每秒 = 16.67毫秒"] + L["30帧每秒 = 33.33毫秒"] + M["10帧每秒 = 100毫秒"] + end + + style B fill:#ffebee + style D fill:#e1f5fe + style E fill:#e8f5e8 + style F fill:#e8f5e8 +``` ```javascript -let gameLoopId = setInterval(() => +const gameLoopId = setInterval(() => { function gameLoop() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; @@ -116,21 +285,33 @@ let gameLoopId = setInterval(() => drawHero(); drawEnemies(); drawStaticObjects(); + } + gameLoop(); }, 200); ``` -上述循环每`200`毫秒调用一次以重绘画布。你可以选择适合你游戏的最佳时间间隔。 +**理解游戏循环结构:** +- **清空**画布,移除上一帧 +- **填充**背景色 +- **绘制**所有游戏对象当前位置 +- **每隔200毫秒**重复上述过程,产生流畅动画 +- **控制**帧率通过定时间隔 -## 继续太空游戏 +## 继续构建太空游戏 -你将扩展现有代码。可以使用你在第一部分完成的代码,也可以使用[第二部分的起始代码](../../../../6-space-game/3-moving-elements-around/your-work)。 +现在给你之前搭建的静态场景添加运动。把它从一张静态截图变成交互体验。我们逐步完成,确保每个部分自然衔接。 -- **移动英雄**:你将添加代码以确保可以使用箭头键移动英雄。 -- **移动敌人**:你还需要添加代码以确保敌人以一定速度从上到下移动。 +从上一课结束的代码开始,或者如果需要全新开始,请使用 [Part II- starter](../../../../6-space-game/3-moving-elements-around/your-work) 文件夹里的代码。 + +**今天要做的是:** +- **英雄控制**:用箭头键驾驶飞船在屏幕内移动 +- **敌人运动**:外星舰开始进攻 + +我们开始实现这些特性。 ## 推荐步骤 -找到在`your-work`子文件夹中为你创建的文件。它应该包含以下内容: +找到为你准备好的文件,位于 `your-work` 子文件夹。里面应包含: ```bash -| assets @@ -141,25 +322,29 @@ let gameLoopId = setInterval(() => -| package.json ``` -通过输入以下命令启动你的项目: +在 `your-work` 文件夹启动项目,运行: ```bash cd your-work npm start ``` -上述命令将在地址`http://localhost:5000`上启动一个HTTP服务器。打开浏览器并输入该地址,现在它应该渲染英雄和所有敌人;目前还没有任何移动! +**此命令做了什么:** +- **切换**到项目目录 +- **启动**HTTP服务器,地址为 `http://localhost:5000` +- **托管**你的游戏文件,可以用浏览器测试 + +以上会在地址 `http://localhost:5000` 启动HTTP服务器。打开浏览器访问它,当前应看到英雄和所有敌机;但暂时还没有运动! ### 添加代码 -1. **为`hero`、`enemy`和`game object`添加专用对象**,它们应该具有`x`和`y`属性。(记住关于[继承或组合](../README.md)的部分)。 +1. **新增独立对象** `hero`,`enemy` 和 `game object`,它们应有 `x` 和 `y` 属性。(回想[继承或组合](../README.md)部分) - *提示*:`game object`应该是具有`x`和`y`属性并能够将自身绘制到画布上的对象。 + *提示* `game object` 应是拥有 `x` 和 `y`,能在画布上自绘的基础类。 + + > **提示**:先新增一个 `GameObject` 类,构造函数如下,后续在画布上绘制它: - >提示:从添加一个新的GameObject类开始,其构造函数如下所示,然后将其绘制到画布上: - ```javascript - class GameObject { constructor(x, y) { this.x = x; @@ -177,12 +362,58 @@ npm start } ``` - 现在,扩展这个GameObject以创建Hero和Enemy。 + **理解此基础类:** + - **定义**所有游戏对象共享的通用属性(位置、尺寸、图片) + - **包含**一个 `dead` 标记,跟踪对象是否应移除 + - **提供**一个 `draw()` 方法,在画布绘制对象 + - **默认设置**所有属性值,子类可覆盖 + +```mermaid +classDiagram + class GameObject { + +x: number + +y: number + +dead: boolean + +type: string + +width: number + +height: number + +img: Image + +draw(ctx) + } + + class Hero { + +speed: number + +type: "英雄" + +width: 98 + +height: 75 + } + + class Enemy { + +type: "敌人" + +width: 98 + +height: 50 + +setInterval() + } + + GameObject <|-- Hero + GameObject <|-- Enemy + + class EventEmitter { + +listeners: object + +on(message, listener) + +emit(message, payload) + } +``` + 现在,继承该 `GameObject` 创建 `Hero` 和 `Enemy`: ```javascript class Hero extends GameObject { constructor(x, y) { - ...it needs an x, y, type, and speed + super(x, y); + this.width = 98; + this.height = 75; + this.type = "Hero"; + this.speed = 5; } } ``` @@ -191,129 +422,173 @@ npm start class Enemy extends GameObject { constructor(x, y) { super(x, y); - (this.width = 98), (this.height = 50); + this.width = 98; + this.height = 50; this.type = "Enemy"; - let id = setInterval(() => { + const id = setInterval(() => { if (this.y < canvas.height - this.height) { this.y += 5; } else { - console.log('Stopped at', this.y) + console.log('Stopped at', this.y); clearInterval(id); } - }, 300) + }, 300); } } ``` -2. **添加键盘事件处理程序**以处理键盘导航(移动英雄上下左右) + **这些类的关键点:** + - **通过** `extends` 关键字继承 `GameObject` + - **用** `super(x, y)` 调用父类构造函数 + - **设置**各自的尺寸与属性 + - **使用** `setInterval()` 实现敌人的自动移动 - *记住*这是一个笛卡尔系统,左上角是`0,0`。还记得要添加代码以停止*默认行为*。 +2. **添加键盘事件处理器**,监听按键实现导航(控制英雄上下左右移动) - >提示:创建你的onKeyDown函数并将其附加到窗口: + *记住* 是笛卡尔坐标系,左上角是 `0,0`。别忘了添加代码阻止 *默认行为*。 - ```javascript - let onKeyDown = function (e) { - console.log(e.keyCode); - ...add the code from the lesson above to stop default behavior - } - }; + > **提示**:创建你的 `onKeyDown` 函数并绑定到 window: - window.addEventListener("keydown", onKeyDown); + ```javascript + const onKeyDown = function (e) { + console.log(e.keyCode); + // 添加上面课文中的代码以阻止默认行为 + switch (e.keyCode) { + case 37: + case 39: + case 38: + case 40: // 方向键 + case 32: + e.preventDefault(); + break; // 空格键 + default: + break; // 不阻止其他按键 + } + }; + + window.addEventListener("keydown", onKeyDown); ``` - 此时检查你的浏览器控制台,观察按键被记录。 - -3. **实现**[发布订阅模式](../README.md),这将使你的代码在后续部分保持整洁。 - - 要完成最后这部分,你可以: - - 1. **在窗口上添加事件监听器**: - - ```javascript - window.addEventListener("keyup", (evt) => { - if (evt.key === "ArrowUp") { - eventEmitter.emit(Messages.KEY_EVENT_UP); - } else if (evt.key === "ArrowDown") { - eventEmitter.emit(Messages.KEY_EVENT_DOWN); - } else if (evt.key === "ArrowLeft") { - eventEmitter.emit(Messages.KEY_EVENT_LEFT); - } else if (evt.key === "ArrowRight") { - eventEmitter.emit(Messages.KEY_EVENT_RIGHT); - } - }); - ``` - - 1. **创建一个EventEmitter类**以发布和订阅消息: + **事件处理器作用:** + - **监听**整个窗口的键盘按下事件 + - **记录**按下的键码,方便调试 + - **阻止**箭头键和空格键的默认行为 + - **允许**其他按键正常工作 + + 此时查看浏览器控制台,观察键击日志。 - ```javascript - class EventEmitter { - constructor() { - this.listeners = {}; - } - - on(message, listener) { - if (!this.listeners[message]) { - this.listeners[message] = []; - } - this.listeners[message].push(listener); - } - - emit(message, payload = null) { - if (this.listeners[message]) { - this.listeners[message].forEach((l) => l(message, payload)); - } - } - } - ``` +3. **实现**[发布-订阅模式](../README.md),使代码保持整洁,便于后续扩展。 - 1. **添加常量**并设置EventEmitter: + 发布-订阅模式帮助组织代码,将事件检测与事件处理分开,使代码模块化,更易维护。 - ```javascript - const Messages = { - KEY_EVENT_UP: "KEY_EVENT_UP", - KEY_EVENT_DOWN: "KEY_EVENT_DOWN", - KEY_EVENT_LEFT: "KEY_EVENT_LEFT", - KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT", - }; - - let heroImg, - enemyImg, - laserImg, - canvas, ctx, - gameObjects = [], - hero, - eventEmitter = new EventEmitter(); - ``` + 要执行这最后一步,可以: - 1. **初始化游戏** + 1. **在 window 上添加事件监听器**: - ```javascript - function initGame() { - gameObjects = []; - createEnemies(); - createHero(); - - eventEmitter.on(Messages.KEY_EVENT_UP, () => { - hero.y -=5 ; - }) + ```javascript + window.addEventListener("keyup", (evt) => { + if (evt.key === "ArrowUp") { + eventEmitter.emit(Messages.KEY_EVENT_UP); + } else if (evt.key === "ArrowDown") { + eventEmitter.emit(Messages.KEY_EVENT_DOWN); + } else if (evt.key === "ArrowLeft") { + eventEmitter.emit(Messages.KEY_EVENT_LEFT); + } else if (evt.key === "ArrowRight") { + eventEmitter.emit(Messages.KEY_EVENT_RIGHT); + } + }); + ``` + + **事件系统作用:** + - **检测**键盘输入,转换为自定义游戏事件 + - **分离**输入检测与游戏逻辑 + - **便于**后续更改控制方式,不影响游戏代码 + - **支持**多个系统响应同一输入 + +```mermaid +flowchart TD + A["键盘输入"] --> B["窗口事件监听器"] + B --> C["事件发射器"] + C --> D["KEY_EVENT_UP"] + C --> E["KEY_EVENT_DOWN"] + C --> F["KEY_EVENT_LEFT"] + C --> G["KEY_EVENT_RIGHT"] - eventEmitter.on(Messages.KEY_EVENT_DOWN, () => { - hero.y += 5; - }); + D --> H["英雄移动"] + D --> I["声音系统"] + D --> J["视觉效果"] - eventEmitter.on(Messages.KEY_EVENT_LEFT, () => { - hero.x -= 5; - }); + E --> H + F --> H + G --> H - eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => { - hero.x += 5; - }); - } - ``` + style A fill:#e1f5fe + style C fill:#e8f5e8 + style H fill:#fff3e0 +``` + 2. **创建 EventEmitter 类**发布和订阅消息: -1. **设置游戏循环** + ```javascript + class EventEmitter { + constructor() { + this.listeners = {}; + } + + on(message, listener) { + if (!this.listeners[message]) { + this.listeners[message] = []; + } + this.listeners[message].push(listener); + } + + 3. **添加常量**,初始化 EventEmitter: + + ```javascript + const Messages = { + KEY_EVENT_UP: "KEY_EVENT_UP", + KEY_EVENT_DOWN: "KEY_EVENT_DOWN", + KEY_EVENT_LEFT: "KEY_EVENT_LEFT", + KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT", + }; + + let heroImg, + enemyImg, + laserImg, + canvas, ctx, + gameObjects = [], + hero, + eventEmitter = new EventEmitter(); + ``` + + **理解此配置:** + - **定义**消息常量,避免拼写错误,便于重构 + - **声明**图像变量、画布上下文及游戏状态 + - **创建**全局事件发射器用于发布-订阅系统 + - **初始化**一个数组以保存所有游戏对象 + + 4. **初始化游戏** - 重构window.onload函数以初始化游戏并以合适的时间间隔设置游戏循环。你还将添加一个激光束: + ```javascript + function initGame() { + gameObjects = []; + createEnemies(); + createHero(); + + eventEmitter.on(Messages.KEY_EVENT_UP, () => { + hero.y -= 5; + }); + + eventEmitter.on(Messages.KEY_EVENT_DOWN, () => { + hero.y += 5; + }); + + eventEmitter.on(Messages.KEY_EVENT_LEFT, () => { + hero.x -= 5; + }); + +4. **设置游戏循环** + + 重构 `window.onload` 函数以初始化游戏并以合适的间隔设置游戏循环。同时,你还将添加一个激光束: ```javascript window.onload = async () => { @@ -324,19 +599,25 @@ npm start laserImg = await loadTexture("assets/laserRed.png"); initGame(); - let gameLoopId = setInterval(() => { + const gameLoopId = setInterval(() => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); drawGameObjects(ctx); - }, 100) - + }, 100); }; ``` -5. **添加代码**以使敌人以一定间隔移动 + **理解游戏设置:** + - **等待**页面完全加载后开始 + - **获取**画布元素及其2D渲染上下文 + - **异步加载**所有图像资源,使用 `await` + - **启动**以100毫秒间隔(10 FPS)运行的游戏循环 + - **每帧**清除并重新绘制整个屏幕 - 重构`createEnemies()`函数以创建敌人并将它们推入新的gameObjects类: +5. **添加代码**以在一定间隔移动敌人 + + 重构 `createEnemies()` 函数以创建敌人并将它们推入新的 gameObjects 类里: ```javascript function createEnemies() { @@ -354,8 +635,14 @@ npm start } } ``` + + **敌人创建做了什么:** + - **计算**位置以使敌人居中显示 + - **使用嵌套循环**创建敌人网格 + - **给每个敌人对象**赋予敌人图像 + - **将每个敌人**添加到全局游戏对象数组中 - 并添加一个`createHero()`函数以对英雄执行类似的过程。 + 并添加一个 `createHero()` 函数对英雄执行类似操作。 ```javascript function createHero() { @@ -368,7 +655,63 @@ npm start } ``` - 最后,添加一个`drawGameObjects()`函数以开始绘制: + **英雄创建做了什么:** + - **将英雄定位**在屏幕底部中央 + - **赋予英雄对象**英雄图像 + - **将英雄添加**到用于渲染的游戏对象数组中 + + 最后,添加一个 `drawGameObjects()` 函数开始绘制: + + ```javascript + function drawGameObjects(ctx) { + gameObjects.forEach(go => go.draw(ctx)); + } + ``` + + **理解绘制函数:** + - **遍历**数组中所有游戏对象 + - **调用**每个对象的 `draw()` 方法 + - **传入**画布上下文让对象自我渲染 + + ### 🔄 **教学检查点** + **完整游戏系统理解**:验证你对整个架构的掌握: + - ✅ 继承如何让 Hero 和 Enemy 共享通用的 GameObject 属性? + - ✅ pub/sub 模式为何能让代码更易维护? + - ✅ 游戏循环在创建流畅动画中起什么作用? + - ✅ 事件监听器如何将用户输入连接到游戏对象行为? + + **系统集成**:你的游戏现已展示: + - **面向对象设计**:基类及专门继承 + - **事件驱动架构**:松耦合的 pub/sub 模式 + - **动画框架**:拥有稳定帧更新的游戏循环 + - **输入处理**:带默认预防的键盘事件 + - **资源管理**:图像加载与精灵渲染 + + **专业模式**:你已实现: + - **关注点分离**:输入、逻辑与渲染分开 + - **多态性**:所有游戏对象共享绘图接口 + - **消息传递**:组件间干净通信 + - **资源管理**:高效的精灵和动画处理 + + 你的敌人应该开始向你的英雄飞船推进! + } + } + ``` + + and add a `createHero()` function to do a similar process for the hero. + + ```javascript + function createHero() { + hero = new Hero( + canvas.width / 2 - 45, + canvas.height - canvas.height / 4 + ); + hero.img = heroImg; + gameObjects.push(hero); + } + ``` + + 最后,添加一个 `drawGameObjects()` 函数开始绘制: ```javascript function drawGameObjects(ctx) { @@ -380,9 +723,33 @@ npm start --- +## GitHub Copilot Agent 挑战 🚀 + +这里有一个挑战,将提升你的游戏精细度:添加屏幕边界和流畅控制。目前,你的英雄可以飞出屏幕,移动可能会感觉不连贯。 + +**你的任务:** 通过实现屏幕边界和流畅移动,让你的飞船感觉更真实。这类似于 NASA 的飞行控制系统防止航天器超过安全操作参数。 + +**你要构建:** 创建一个系统保持你的英雄飞船在屏幕内,并让控制感觉顺滑。当玩家按住方向键时,飞船应连续滑动,而不是离散移动。考虑在飞船触及屏幕边界时添加视觉反馈——或许是微妙的效果提醒玩家已到达游戏区边缘。 + +了解更多关于 [agent 模式](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode)。 + ## 🚀 挑战 -如你所见,当你开始添加函数、变量和类时,你的代码可能会变成“意大利面条代码”。你如何更好地组织代码,使其更易读?画出一个系统来组织你的代码,即使它仍然在一个文件中。 +随着项目增长,代码组织变得越来越重要。你可能已经注意到文件被函数、变量和类混在一起挤满了。这让我想到阿波罗任务的工程师们,他们必须创建清晰、可维护的系统,让多个团队能够同时协作开发。 + +**你的使命:** +像软件架构师一样思考。你会如何组织代码,以便六个月后,无论是你还是队友,都能理解发生了什么?即使暂时所有代码都在一个文件中,也可以更好地组织: + +- **将相关函数分组**,并用清晰的注释标题 +- **分离关注点** — 保持游戏逻辑和渲染分开 +- **使用一致命名** 规范给变量和函数命名 +- **创建模块**或命名空间组织游戏不同部分 +- **添加文档**解释每个主要部分的目的 + +**反思问题:** +- 回头看时,代码中哪些部分最难理解? +- 如何组织代码让其他人更容易贡献? +- 如果想添加道具或不同敌人类型,会发生什么? ## 课后测验 @@ -390,13 +757,143 @@ npm start ## 复习与自学 -虽然我们在没有使用框架的情况下编写游戏,但有许多基于JavaScript的画布框架可用于游戏开发。花些时间阅读[相关内容](https://github.com/collections/javascript-game-engines)。 +我们一直从零开始构建,这对学习非常棒,但这里有个小秘密——有一些了不起的 JavaScript 框架能帮你处理大量繁重工作。掌握我们覆盖的基础后,值得去 [探索现有资源](https://github.com/collections/javascript-game-engines)。 + +把框架看作是工具箱里现成的工具,而不是手工制造每个工具。它们能解决许多代码组织难题,还提供需要几周才能自己写出的功能。 -## 作业 +**值得探索的内容:** +- 游戏引擎如何组织代码——你会惊讶于它们巧妙的模式 +- 提升画布游戏性能的小技巧 +- 现代 JavaScript 特性如何让代码更简洁易维护 +- 管理游戏对象及其关系的不同方法 -[为你的代码添加注释](assignment.md) +## 🎯 你的游戏动画掌握时间表 + +```mermaid +timeline + title 游戏动画与交互学习进程 + + section 运动基础(20分钟) + 动画原理: 基于帧的动画 + : 位置更新 + : 坐标系统 + : 平滑移动 + + section 事件系统(25分钟) + 用户输入: 键盘事件处理 + : 默认行为阻止 + : 事件对象属性 + : 窗口级监听 + + section 游戏架构(30分钟) + 对象设计: 继承模式 + : 基类创建 + : 专用行为 + : 多态接口 + + section 通信模式(35分钟) + 发布/订阅实现: 事件触发器 + : 消息常量 + : 松耦合 + : 系统集成 + + section 游戏循环精通(40分钟) + 实时系统: 帧率控制 + : 更新/渲染循环 + : 状态管理 + : 性能优化 + + section 高级技巧(45分钟) + 专业特性: 碰撞检测 + : 物理模拟 + : 状态机 + : 组件系统 + + section 游戏引擎概念(一周) + 框架理解: 实体组件系统 + : 场景图 + : 资源流水线 + : 性能分析 + + section 生产技能(一月) + 专业发展: 代码组织 + : 团队协作 + : 测试策略 + : 部署优化 +``` +### 🛠️ 你的游戏开发工具包总结 + +完成本课后,你已经掌握了: +- **动画原理**:基于帧的运动和平滑过渡 +- **事件驱动编程**:键盘输入处理及事件管理 +- **面向对象设计**:继承层次和多态接口 +- **通信模式**:易维护的 pub/sub 架构 +- **游戏循环架构**:实时更新和渲染周期 +- **输入系统**:用户控制映射及默认行为预防 +- **资源管理**:精灵加载和高效渲染技术 + +### ⚡ **你接下来5分钟能做的事** +- [ ] 打开浏览器控制台,试试 `addEventListener('keydown', console.log)` 观察键盘事件 +- [ ] 创建一个简单的 div,并用箭头键移动它 +- [ ] 试用 `setInterval` 实现持续移动 +- [ ] 尝试用 `event.preventDefault()` 阻止默认行为 + +### 🎯 **你这一小时能完成的任务** +- [ ] 完成课后测验,理解事件驱动编程 +- [ ] 构建带完整键盘控制的移动英雄飞船 +- [ ] 实现流畅的敌人移动模式 +- [ ] 添加边界,防止游戏对象离开屏幕 +- [ ] 创建基本的游戏对象碰撞检测 + +### 📅 **你的为期一周的动画旅程** +- [ ] 完成带精细移动和互动的完整太空游戏 +- [ ] 添加高级移动模式,如曲线、加速和物理效果 +- [ ] 实现平滑过渡和缓动函数 +- [ ] 创建粒子效果和视觉反馈系统 +- [ ] 优化游戏性能,实现流畅60fps体验 +- [ ] 添加手机触控控制和响应式设计 + +### 🌟 **你的为期一个月的交互开发** +- [ ] 构建带高级动画系统的复杂交互应用 +- [ ] 学习动画库如 GSAP 或自己创建动画引擎 +- [ ] 参与开源游戏开发和动画项目 +- [ ] 掌握图形密集型应用的性能优化 +- [ ] 创建关于游戏开发和动画的教育内容 +- [ ] 构建展示高级交互编程技能的作品集 + +**现实应用场景**:你的游戏动画技能直接应用于: +- **交互式网页应用**:动态仪表盘与实时界面 +- **数据可视化**:动画图表和互动图形 +- **教育软件**:交互式仿真和学习工具 +- **移动开发**:基于触摸的游戏和手势处理 +- **桌面应用**:带流畅动画的 Electron 应用 +- **网页动画**:CSS 和 JavaScript 动画库 + +**获得的专业技能**: +- **架构设计**可扩展的事件驱动系统 +- **实现**基于数学原理的平滑动画 +- **调试**复杂交互系统,使用浏览器开发者工具 +- **优化**不同设备和浏览器的游戏性能 +- **设计**使用成熟模式的可维护代码结构 + +**掌握的游戏开发概念**: +- **帧率管理**:理解 FPS 和时序控制 +- **输入处理**:跨平台键盘和事件系统 +- **对象生命周期**:创建、更新与销毁模式 +- **状态同步**:保持游戏状态在帧间一致 +- **事件架构**:游戏系统间解耦通信 + +**下一步**:你已准备好添加碰撞检测、得分系统、音效,或探索现代游戏框架如 Phaser 或 Three.js! + +🌟 **成就解锁**:你已经构建了一个完整的交互游戏系统,采用了专业的架构模式! + +## 任务 + +[给你的代码添加注释](assignment.md) --- + **免责声明**: -本文档使用AI翻译服务[Co-op Translator](https://github.com/Azure/co-op-translator)进行翻译。尽管我们努力确保准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。对于因使用本翻译而引起的任何误解或误读,我们概不负责。 \ No newline at end of file +本文档采用AI翻译服务[Co-op Translator](https://github.com/Azure/co-op-translator)进行翻译。尽管我们力求准确,但请注意自动翻译可能存在错误或不准确之处。原始语言版本的文档应被视为权威来源。对于关键信息,建议使用专业人工翻译。对于因使用本翻译而产生的任何误解或误释,我们概不负责。 + \ No newline at end of file diff --git a/translations/zh/6-space-game/3-moving-elements-around/assignment.md b/translations/zh/6-space-game/3-moving-elements-around/assignment.md index 767778c89..66bd60037 100644 --- a/translations/zh/6-space-game/3-moving-elements-around/assignment.md +++ b/translations/zh/6-space-game/3-moving-elements-around/assignment.md @@ -1,23 +1,38 @@ -# 注释你的代码 +# 评论你的代码 -## 指导 +## 指南 -查看游戏文件夹中的当前 /app.js 文件,寻找方法为其添加注释并整理代码。代码很容易变得混乱,现在是一个好机会添加注释,确保代码可读,以便以后使用。 +清晰、良好注释的代码对于维护和共享项目至关重要。在本作业中,您将练习专业开发者的一个重要习惯:编写清晰、有帮助的注释,解释代码的目的和功能。 + +检查你游戏文件夹中的当前 `app.js` 文件,找出为其添加注释和整理代码的方法。代码很容易失控,现在是添加注释的好机会,以确保你拥有可读的代码,便于以后使用。 + +**你的任务包括:** +- **添加注释**,解释每个主要代码部分的作用 +- **文档化函数**,清晰描述其目的和参数 +- **将代码组织** 成逻辑模块,并添加部分标题 +- **删除** 任何未使用或冗余的代码 +- **使用一致的** 变量和函数命名规范 ## 评分标准 -| 标准 | 卓越表现 | 合格表现 | 需要改进 | -| -------- | ---------------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------- | -| | `app.js` 代码完全注释并组织成逻辑块 | `app.js` 代码有适当的注释 | `app.js` 代码有些混乱,缺乏良好的注释 | +| 评估标准 | 优秀 | 及格 | 需要改进 | +| -------- | --------- | -------- | ----------------- | +| **代码文档** | `app.js` 代码完全注释,对所有主要部分和函数有清晰、有帮助的解释 | `app.js` 代码有充分注释,对大多数部分有基本说明 | `app.js` 代码注释很少,缺乏清晰的说明 | +| **代码组织** | 代码组织成逻辑模块,有清晰的部分标题和一致的结构 | 代码有一定组织,对相关功能进行了基本分组 | 代码有些杂乱,难以理解 | +| **代码质量** | 所有变量和函数使用描述性名称,无未使用代码,风格一致 | 大部分代码使用良好命名,未使用代码较少 | 变量命名不清晰,含未使用代码,风格不一致 | + +--- + **免责声明**: -本文档使用AI翻译服务 [Co-op Translator](https://github.com/Azure/co-op-translator) 进行翻译。尽管我们努力确保翻译的准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。因使用本翻译而导致的任何误解或误读,我们概不负责。 \ No newline at end of file +本文件使用 AI 翻译服务 [Co-op Translator](https://github.com/Azure/co-op-translator) 进行翻译。虽然我们力求准确,但请注意自动翻译可能包含错误或不准确之处。原始语言的文档应被视为权威来源。对于重要信息,建议使用专业人工翻译。我们不对因使用本翻译而产生的任何误解或误释承担责任。 + \ No newline at end of file diff --git a/translations/zh/6-space-game/4-collision-detection/README.md b/translations/zh/6-space-game/4-collision-detection/README.md index 7a341f663..b0faac2ff 100644 --- a/translations/zh/6-space-game/4-collision-detection/README.md +++ b/translations/zh/6-space-game/4-collision-detection/README.md @@ -1,131 +1,325 @@ -# 构建太空游戏第四部分:添加激光和检测碰撞 - +# 构建太空游戏 第4部分:添加激光枪并检测碰撞 + +```mermaid +journey + title 你的碰撞检测之旅 + section 物理基础 + 理解矩形: 3: Student + 学习交叉数学: 4: Student + 掌握坐标系: 4: Student + section 游戏机制 + 实现激光发射: 4: Student + 添加对象生命周期: 5: Student + 创建碰撞规则: 5: Student + section 系统集成 + 构建碰撞检测: 5: Student + 优化性能: 5: Student + 测试交互系统: 5: Student +``` ## 课前测验 [课前测验](https://ff-quizzes.netlify.app/web/quiz/35) -在本课中,你将学习如何使用 JavaScript 发射激光!我们将为游戏添加以下两项内容: - -- **激光**:从英雄的飞船发射,垂直向上移动。 -- **碰撞检测**:作为实现射击功能的一部分,我们还会添加一些有趣的游戏规则: - - **激光击中敌人**:敌人被激光击中后会死亡。 - - **激光击中屏幕顶部**:激光击中屏幕顶部后会被销毁。 - - **敌人与英雄碰撞**:敌人与英雄相撞后双方都会被销毁。 - - **敌人到达屏幕底部**:敌人到达屏幕底部后,敌人和英雄都会被销毁。 - -简而言之,你——*英雄*——需要在敌人到达屏幕底部之前用激光击中所有敌人。 - -✅ 研究一下历史上第一个计算机游戏的功能是什么? - -让我们一起成为英雄吧! +想想《星球大战》中卢克的质子鱼雷击中死星排气口的那一刻。那精确的碰撞检测改变了银河系的命运!在游戏中,碰撞检测也一样——它决定了物体何时交互以及接下来发生什么。 + +在本课中,你将为你的太空游戏添加激光武器并实现碰撞检测。就像 NASA 任务规划人员计算航天器轨迹以避免碎片一样,你将学会检测游戏对象何时相交。我们会分解成相互构建的可管理步骤。 + +到课末,你将拥有一个功能完整的战斗系统,激光可以摧毁敌人,碰撞会触发游戏事件。这些碰撞原理被广泛应用于从物理模拟到互动网页界面的所有领域。 + +```mermaid +mindmap + root((碰撞检测)) + Physics Concepts + Rectangle Boundaries + Intersection Testing + Coordinate Systems + Separation Logic + Game Objects + Laser Projectiles + Enemy Ships + Hero Character + Collision Zones + Lifecycle Management + Object Creation + Movement Updates + Destruction Marking + Memory Cleanup + Event Systems + Keyboard Input + Collision Events + Game State Changes + Audio/Visual Effects + Performance + Efficient Algorithms + Frame Rate Optimization + Memory Management + Spatial Partitioning +``` +✅ 做一些关于有史以来第一个计算机游戏的调研。它的功能是什么? ## 碰撞检测 -如何进行碰撞检测?我们需要将游戏中的对象视为移动的矩形。为什么要这样做呢?因为用于绘制游戏对象的图像是一个矩形:它有 `x`、`y`、`width` 和 `height`。 +碰撞检测就像阿波罗登月舱的接近传感器——它不断检查距离,当物体靠得太近时触发警报。在游戏中,这个系统判断物体何时交互及后续动作。 -如果两个矩形(例如英雄和敌人)*相交*,就发生了碰撞。碰撞后应该发生什么取决于游戏规则。为了实现碰撞检测,你需要以下内容: +我们使用的方法将每个游戏对象视为矩形,就像空中交通控制系统使用简化的几何形状来跟踪飞机一样。这个矩形方法看似简单,但计算效率高,适合大多数游戏场景。 -1. 获取游戏对象的矩形表示方法,例如: +### 矩形表示 - ```javascript - rectFromGameObject() { - return { - top: this.y, - left: this.x, - bottom: this.y + this.height, - right: this.x + this.width - } - } - ``` +每个游戏对象都需要坐标边界,类似火星探路者漫游车如何映射其在火星表面的位置。以下是我们定义边界坐标的方法: -2. 比较函数,这个函数可以像这样: +```mermaid +flowchart TD + A["🎯 游戏对象"] --> B["📍 位置 (x, y)"] + A --> C["📏 尺寸 (宽度, 高度)"] + + B --> D["顶部: y"] + B --> E["左侧: x"] + + C --> F["底部: y + 高度"] + C --> G["右侧: x + 宽度"] + + D --> H["🔲 矩形边界"] + E --> H + F --> H + G --> H + + H --> I["碰撞检测准备就绪"] + + style A fill:#e3f2fd + style H fill:#e8f5e8 + style I fill:#fff3e0 +``` +```javascript +rectFromGameObject() { + return { + top: this.y, + left: this.x, + bottom: this.y + this.height, + right: this.x + this.width + } +} +``` + +**我们来拆解一下:** +- **上边缘**:就是对象的垂直起点(它的 y 坐标) +- **左边缘**:对象的水平起点(它的 x 坐标) +- **下边缘**:加上高度后的 y 坐标,知道它的底部位置! +- **右边缘**:加上宽度后的 x 坐标,完整边界就确定了 - ```javascript - function intersectRect(r1, r2) { - return !(r2.left > r1.right || - r2.right < r1.left || - r2.top > r1.bottom || - r2.bottom < r1.top); - } - ``` +### 相交算法 -## 如何销毁对象 +检测矩形相交的逻辑类似哈勃太空望远镜判断天体是否重叠视场。算法检查两个矩形是否存在分离: -在游戏中销毁对象需要让游戏知道在某个时间间隔触发的游戏循环中不再绘制该对象。实现方法是当某些事件发生时将游戏对象标记为*死亡*,例如: +```mermaid +flowchart LR + A["矩形 1"] --> B{"分离测试"} + C["矩形 2"] --> B + + B --> D["R2 左侧 > R1 右侧?"] + B --> E["R2 右侧 < R1 左侧?"] + B --> F["R2 顶部 > R1 底部?"] + B --> G["R2 底部 < R1 顶部?"] + + D --> H{"有任一为真?"} + E --> H + F --> H + G --> H + + H -->|是| I["❌ 无碰撞"] + H -->|否| J["✅ 检测到碰撞"] + + style B fill:#e3f2fd + style I fill:#ffebee + style J fill:#e8f5e8 +``` +```javascript +function intersectRect(r1, r2) { + return !(r2.left > r1.right || + r2.right < r1.left || + r2.top > r1.bottom || + r2.bottom < r1.top); +} +``` + +**分离测试工作原理就像雷达系统:** +- 矩形 2 是否完全在矩形 1 的右侧? +- 矩形 2 是否完全在矩形 1 的左侧? +- 矩形 2 是否完全在矩形 1 的下面? +- 矩形 2 是否完全在矩形 1 的上面? + +如果上述条件都不成立,矩形必然重叠。这种方法类似雷达操作员确定两架飞机是否保持安全距离。 + +## 管理对象生命周期 + +当激光击中敌人时,两个对象都需要从游戏中删除。然而,在循环过程中直接删除对象可能导致崩溃——这是早期计算机系统如阿波罗导航计算机走过的弯路。我们采用“标记删除”的方法,在帧与帧之间安全移除对象。 + +```mermaid +stateDiagram-v2 + [*] --> Active: 对象创建 + Active --> Collided: 检测到碰撞 + Collided --> MarkedDead: 设定 dead = true + MarkedDead --> Filtered: 下一帧 + Filtered --> [*]: 对象移除 + + Active --> OutOfBounds: 离开屏幕 + OutOfBounds --> MarkedDead + + note right of MarkedDead + 当前帧 + 继续安全 + end note + + note right of Filtered + 帧间 + 移除对象 + end note +``` +这是我们标记删除的方法: ```javascript -// collision happened -enemy.dead = true +// 标记对象待移除 +enemy.dead = true; ``` + +**这种方法的优点:** +- 标记对象为“死亡”,但暂不删除 +- 允许当前游戏帧安全完成 +- 避免因使用已删除对象而崩溃! -然后在重新绘制屏幕之前处理*死亡*对象,例如: +然后在下一渲染周期前过滤标记的对象: ```javascript -gameObjects = gameObject.filter(go => !go.dead); +gameObjects = gameObjects.filter(go => !go.dead); ``` + +**此过滤操作作用:** +- 创建只包含“存活”对象的新列表 +- 丢弃被标记为死亡的对象 +- 保持游戏流畅运行 +- 防止销毁对象累积导致内存膨胀 + +## 实现激光机制 + +游戏中的激光投射物类似于《星际迷航》中的光子鱼雷——它们是沿直线飞行的离散物体直到命中目标。每次按空格键会创建一个新的激光对象,在屏幕上移动。 -## 如何发射激光 +要实现这个功能,我们需要协调几个部分: -发射激光意味着响应按键事件并创建一个向某个方向移动的对象。因此,我们需要完成以下步骤: +**关键组件:** +- **创建** 从英雄位置生成的激光对象 +- **处理** 键盘输入以触发激光创建 +- **管理** 激光运动和生命周期 +- **实现** 激光投射物的视觉表现 -1. **创建激光对象**:从英雄飞船顶部发射,创建后开始向屏幕顶部移动。 -2. **绑定按键事件代码**:选择键盘上的某个按键来代表玩家发射激光。 -3. **创建一个看起来像激光的游戏对象**:当按键被按下时。 +## 实现开火速率控制 -## 激光的冷却时间 +无限制的开火速率会让游戏引擎超负荷,也让游戏太容易。真实武器系统也有类似限制——即使是企业号的相位枪也需要时间充能。 -激光需要在每次按键时发射,例如按下*空格键*。为了防止游戏在短时间内生成过多激光,我们需要解决这个问题。解决方法是实现所谓的*冷却时间*,即一个计时器,确保激光只能以一定频率发射。可以这样实现: +我们实现冷却系统防止速射刷屏,同时保持响应: +```mermaid +sequenceDiagram + participant Player + participant Weapon + participant Cooldown + participant Game + + Player->>Weapon: 按下空格键 + Weapon->>Cooldown: 检查是否冷却 + + alt 武器已准备好 + Cooldown->>Weapon: cool = true + Weapon->>Game: 创建激光 + Weapon->>Cooldown: 开始新的冷却 + Cooldown->>Cooldown: cool = false + + Note over Cooldown: 等待 500 毫秒 + + Cooldown->>Cooldown: cool = true + else 武器正在冷却 + Cooldown->>Weapon: cool = false + Weapon->>Player: 无操作 + end +``` ```javascript class Cooldown { constructor(time) { this.cool = false; setTimeout(() => { this.cool = true; - }, time) + }, time); } } class Weapon { - constructor { + constructor() { + this.cooldown = null; } + fire() { if (!this.cooldown || this.cooldown.cool) { - // produce a laser + // 创建激光投射物 this.cooldown = new Cooldown(500); } else { - // do nothing - it hasn't cooled down yet. + // 武器仍在冷却中 } } } ``` + +**冷却系统原理:** +- 创建时武器处于“热”状态(暂不可发射) +- 经过超时时间后变“冷”状态(准备发射) +- 发射前检查:武器是否处于“冷”状态? +- 防止连点刷屏,响应仍然灵敏 + +✅ 参考太空游戏第一课复习冷却机制。 + +## 构建碰撞系统 + +你将扩展已有太空游戏代码,实现碰撞检测系统。像国际空间站自动避碰系统一样,你的游戏会持续监控对象位置并响应相交。 + +从之前的课程代码出发,添加带规则的碰撞检测,管理对象交互。 + +> 💡 **小技巧**:激光精灵已包含在资源文件夹并在代码中引用,准备好使用。 -✅ 回顾太空游戏系列第一课,了解有关*冷却时间*的内容。 +### 要实现的碰撞规则 -## 要构建的内容 +**游戏机制:** +1. **激光击中敌人**:激光击中敌人时,敌人被摧毁 +2. **激光触及屏幕边界**:激光到达屏幕顶部时被移除 +3. **敌人与英雄碰撞**:两者交叉时均被摧毁 +4. **敌人到底部**:敌人到达屏幕底部时游戏结束 -你将使用上一课中清理和重构过的代码进行扩展。可以从第二部分的代码开始,也可以使用[第三部分的起始代码](../../../../../../../../../your-work)。 +### 🔄 **教学检查点** +**碰撞检测基础**:实现前确认自己理解: +- ✅ 矩形边界如何定义碰撞区域 +- ✅ 为什么分离测试比求交效率高 +- ✅ 对象生命周期管理为何游戏循环关键 +- ✅ 事件驱动系统协调碰撞响应的方法 -> 提示:你将使用的激光已经在你的资源文件夹中,并且代码中已经引用了它。 +**快速自测**:如果你立即删除对象会发生什么? +*答:循环中间删除可能导致崩溃或漏遍历* -- **添加碰撞检测**:当激光与某物体碰撞时,应该遵循以下规则: - 1. **激光击中敌人**:敌人被激光击中后会死亡。 - 2. **激光击中屏幕顶部**:激光击中屏幕顶部后会被销毁。 - 3. **敌人与英雄碰撞**:敌人与英雄相撞后双方都会被销毁。 - 4. **敌人到达屏幕底部**:敌人到达屏幕底部后,敌人和英雄都会被销毁。 +**物理理解**:你现已掌握: +- **坐标系**:位置与尺寸定义边界 +- **相交逻辑**:碰撞检测中的数学原理 +- **性能优化**:实时系统中高效算法意义 +- **内存管理**:保证稳定性的安全生命周期模式 -## 推荐步骤 +## 设置开发环境 -找到在 `your-work` 子文件夹中为你创建的文件。它应该包含以下内容: +好消息——大部分基础已帮你搭好!所有游戏资源和基本结构都位于 `your-work` 子文件夹,等待你添加酷炫的碰撞功能。 + +### 项目结构 ```bash -| assets @@ -136,162 +330,431 @@ class Weapon { -| app.js -| package.json ``` + +**文件结构说明:** +- **包含** 游戏对象所需全部精灵图像 +- **含有** 主要 HTML 文档和 JavaScript 应用文件 +- **提供** 本地开发服务器的配置文件 + +### 启动开发服务器 -通过输入以下命令启动项目: +进入项目文件夹启动本地服务器: ```bash cd your-work npm start ``` + +**此命令执行步骤:** +- **切换** 到你的工作项目目录 +- **启动** 本地 HTTP 服务器,地址为 `http://localhost:5000` +- **提供** 游戏文件供测试和开发使用 +- **支持** 自动刷新实现实时开发 + +打开浏览器访问 `http://localhost:5000`,你将看到当前游戏状态,英雄和敌人呈现屏幕上。 + +### 逐步实现 + +如同 NASA 规划航海者号软件那样,我们将有系统地逐步实现碰撞检测功能。 + +```mermaid +flowchart TD + A["1. 矩形边界"] --> B["2. 相交检测"] + B --> C["3. 激光系统"] + C --> D["4. 事件处理"] + D --> E["5. 碰撞规则"] + E --> F["6. 冷却系统"] + + G["对象边界"] --> A + H["物理算法"] --> B + I["投射物创建"] --> C + J["键盘输入"] --> D + K["游戏逻辑"] --> E + L["速率限制"] --> F + + F --> M["🎮 完整游戏"] + + style A fill:#e3f2fd + style B fill:#e8f5e8 + style C fill:#fff3e0 + style D fill:#f3e5f5 + style E fill:#e0f2f1 + style F fill:#fce4ec + style M fill:#e1f5fe +``` +#### 1. 添加矩形碰撞边界 + +首先教你的游戏对象如何描述自身边界。将此方法添加至你的 `GameObject` 类: -上述命令将在地址 `http://localhost:5000` 上启动一个 HTTP 服务器。打开浏览器并输入该地址,目前应该可以渲染英雄和所有敌人,但它们还不会移动。 +```javascript +rectFromGameObject() { + return { + top: this.y, + left: this.x, + bottom: this.y + this.height, + right: this.x + this.width, + }; + } +``` + +**此方法作用:** +- **创建** 带精确边界坐标的矩形对象 +- **计算** 底边和右边坐标(位置加尺寸) +- **返回** 可供碰撞检测算法使用的对象 +- **提供** 给所有游戏对象统一接口 + +#### 2. 实现相交检测 + +接下来创建碰撞侦测函数,用于判断两个矩形是否重叠: + +```javascript +function intersectRect(r1, r2) { + return !( + r2.left > r1.right || + r2.right < r1.left || + r2.top > r1.bottom || + r2.bottom < r1.top + ); +} +``` + +**算法逻辑:** +- **测试** 矩形间四种分离情况 +- **如果** 存在分离,则返回 `false`(无碰撞) +- **无分离时** 表示发生碰撞,返回 `true` +- **使用** 取反逻辑提高效率 -### 添加代码 +#### 3. 实现激光发射系统 + +现在进入令人激动的部分!我们设置激光发射功能。 + +##### 消息常量 + +先定义消息类型,便于游戏各部分通信: + +```javascript +KEY_EVENT_SPACE: "KEY_EVENT_SPACE", +COLLISION_ENEMY_LASER: "COLLISION_ENEMY_LASER", +COLLISION_ENEMY_HERO: "COLLISION_ENEMY_HERO", +``` + +**这些常量提供:** +- **统一** 应用程序内事件命名 +- **确保** 各系统通信一致性 +- **避免** 事件处理时拼写错误 -1. **设置游戏对象的矩形表示方法以处理碰撞** 以下代码允许你获取 `GameObject` 的矩形表示方法。编辑你的 GameObject 类以扩展它: +##### 键盘输入处理 - ```javascript - rectFromGameObject() { - return { - top: this.y, - left: this.x, - bottom: this.y + this.height, - right: this.x + this.width, - }; +在键盘事件监听器中添加空格键检测: + +```javascript +} else if(evt.keyCode === 32) { + eventEmitter.emit(Messages.KEY_EVENT_SPACE); +} +``` + +**该输入处理器:** +- **监听** keyCode 32 空格键 +- **发送** 标准化事件消息 +- **实现** 触发发射逻辑的解耦 + +##### 事件监听设置 + +在你的 `initGame()` 函数中注册发射行为: + +```javascript +eventEmitter.on(Messages.KEY_EVENT_SPACE, () => { + if (hero.canFire()) { + hero.fire(); + } +}); +``` + +**事件监听器:** +- **响应** 空格键事件 +- **检查** 冷却状态是否允许发射 +- **允许时** 触发激光对象创建 + +添加激光与敌人碰撞事件处理: + +```javascript +eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => { + first.dead = true; + second.dead = true; +}); +``` + +**碰撞处理器:** +- **接收** 碰撞事件数据,包括两个对象 +- **标记** 两个对象为待删除 +- **确保** 碰撞后正确清理 + +#### 4. 创建激光类 + +实现激光投射物类,使其向上移动并管理生命周期: + +```javascript +class Laser extends GameObject { + constructor(x, y) { + super(x, y); + this.width = 9; + this.height = 33; + this.type = 'Laser'; + this.img = laserImg; + + let id = setInterval(() => { + if (this.y > 0) { + this.y -= 15; + } else { + this.dead = true; + clearInterval(id); } - ``` - -2. **添加检测碰撞的代码** 这是一个新函数,用于测试两个矩形是否相交: - - ```javascript - function intersectRect(r1, r2) { - return !( - r2.left > r1.right || - r2.right < r1.left || - r2.top > r1.bottom || - r2.bottom < r1.top - ); - } - ``` - -3. **添加激光发射功能** - 1. **添加按键事件消息**。按下*空格键*时应该在英雄飞船上方创建一个激光。为 Messages 对象添加三个常量: - - ```javascript - KEY_EVENT_SPACE: "KEY_EVENT_SPACE", - COLLISION_ENEMY_LASER: "COLLISION_ENEMY_LASER", - COLLISION_ENEMY_HERO: "COLLISION_ENEMY_HERO", - ``` - - 1. **处理空格键**。编辑 `window.addEventListener` 的 keyup 函数以处理空格键: - - ```javascript - } else if(evt.keyCode === 32) { - eventEmitter.emit(Messages.KEY_EVENT_SPACE); - } - ``` - - 1. **添加监听器**。编辑 `initGame()` 函数以确保按下空格键时英雄可以发射激光: - - ```javascript - eventEmitter.on(Messages.KEY_EVENT_SPACE, () => { - if (hero.canFire()) { - hero.fire(); - } - ``` - - 并添加一个新的 `eventEmitter.on()` 函数以确保当敌人与激光碰撞时的行为: - - ```javascript - eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => { - first.dead = true; - second.dead = true; - }) - ``` - - 1. **移动对象**,确保激光逐渐移动到屏幕顶部。你将创建一个新的 Laser 类,继承 `GameObject`,如之前所做: - - ```javascript - class Laser extends GameObject { - constructor(x, y) { - super(x,y); - (this.width = 9), (this.height = 33); - this.type = 'Laser'; - this.img = laserImg; - let id = setInterval(() => { - if (this.y > 0) { - this.y -= 15; - } else { - this.dead = true; - clearInterval(id); - } - }, 100) - } + }, 100); + } +} +``` + +**此类实现:** +- **继承** GameObject 以获得基础功能 +- **设置** 合适的激光精灵尺寸 +- **使用** setInterval() 实现自动向上移动 +- **在** 到达顶部时自我销毁 +- **管理** 自己的动画时间和清理 + +#### 5. 实现碰撞检测系统 + +创建完整碰撞检测函数: + +```javascript +function updateGameObjects() { + const enemies = gameObjects.filter(go => go.type === 'Enemy'); + const lasers = gameObjects.filter(go => go.type === "Laser"); + + // 测试激光与敌人的碰撞 + lasers.forEach((laser) => { + enemies.forEach((enemy) => { + if (intersectRect(laser.rectFromGameObject(), enemy.rectFromGameObject())) { + eventEmitter.emit(Messages.COLLISION_ENEMY_LASER, { + first: laser, + second: enemy, + }); } - ``` - - 1. **处理碰撞**,实现激光的碰撞规则。添加一个 `updateGameObjects()` 函数,用于测试碰撞对象是否发生击中: - - ```javascript - function updateGameObjects() { - const enemies = gameObjects.filter(go => go.type === 'Enemy'); - const lasers = gameObjects.filter((go) => go.type === "Laser"); - // laser hit something - lasers.forEach((l) => { - enemies.forEach((m) => { - if (intersectRect(l.rectFromGameObject(), m.rectFromGameObject())) { - eventEmitter.emit(Messages.COLLISION_ENEMY_LASER, { - first: l, - second: m, - }); - } - }); - }); - - gameObjects = gameObjects.filter(go => !go.dead); - } - ``` - - 确保在 `window.onload` 的游戏循环中添加 `updateGameObjects()`。 - - 4. **实现激光的冷却时间**,确保激光只能以一定频率发射。 - - 最后,编辑 Hero 类以实现冷却功能: - - ```javascript - class Hero extends GameObject { - constructor(x, y) { - super(x, y); - (this.width = 99), (this.height = 75); - this.type = "Hero"; - this.speed = { x: 0, y: 0 }; - this.cooldown = 0; - } - fire() { - gameObjects.push(new Laser(this.x + 45, this.y - 10)); - this.cooldown = 500; - - let id = setInterval(() => { - if (this.cooldown > 0) { - this.cooldown -= 100; - } else { - clearInterval(id); - } - }, 200); - } - canFire() { - return this.cooldown === 0; - } + }); + }); + + // 移除已销毁的对象 + gameObjects = gameObjects.filter(go => !go.dead); +} +``` + +**该碰撞系统:** +- **根据类型** 过滤游戏对象提高测试效率 +- **测试** 所有激光与所有敌人是否相交 +- **发现碰撞时** 发送碰撞事件 +- **碰撞处理后** 清理已销毁对象 + +> ⚠️ **重要**:在主游戏循环(`window.onload`)中添加 `updateGameObjects()` 以启用碰撞检测。 + +#### 6. 为英雄类添加冷却系统 + +为英雄类增强发射机制和速率控制: + +```javascript +class Hero extends GameObject { + constructor(x, y) { + super(x, y); + this.width = 99; + this.height = 75; + this.type = "Hero"; + this.speed = { x: 0, y: 0 }; + this.cooldown = 0; + } + + fire() { + gameObjects.push(new Laser(this.x + 45, this.y - 10)); + this.cooldown = 500; + + let id = setInterval(() => { + if (this.cooldown > 0) { + this.cooldown -= 100; + } else { + clearInterval(id); } - ``` + }, 200); + } + + canFire() { + return this.cooldown === 0; + } +} +``` + +**增强后的英雄类解析:** +- **初始化** 冷却计时器为零(处于可发射状态) +- **创建** 激光对象,位置设定于英雄舰船上方 +- **设置** 冷却周期防止连续快速发射 +- **通过** 定时器递减冷却时间 +- **提供** `canFire()` 方法判断是否可发射 + +### 🔄 **教学检查点** +**完整系统理解**:确认你掌握碰撞系统: +- ✅ 矩形边界怎样实现高效碰撞检测? +- ✅ 为什么对象生命周期管理对游戏稳定关键? +- ✅ 冷却系统如何避免性能问题? +- ✅ 事件驱动框架在碰撞处理中的作用? + +**系统整合**:你的碰撞检测系统体现了: +- **数学精度**:矩形相交算法 +- **性能优化**:高效碰撞测试模式 +- **内存管理**:安全创建与销毁对象 +- **事件协调**:解耦系统通信 +- **实时处理**:基于帧的更新机制 + +**专业模式**:你已经实现了: +- **职责分离**:物理、渲染与输入解耦 +- **面向对象设计**:继承与多态 +- **状态管理**:对象生命周期和游戏状态跟踪 +- **性能优化**:适合实时的高效算法 + +### 测试你的实现 + +你的太空游戏现已具备完整碰撞检测与战斗机制。🚀 测试这些新功能: +- **用箭头键** 验证移动控制 +- **按空格键** 发射激光,注意冷却防止速射刷屏 +- **观察碰撞效果** :激光击中敌人触发移除 +- **验证清理** :被摧毁对象从游戏中消失 + +你已成功实现一个基于航天导航与机器人技术数学原理的碰撞检测系统。 + +### ⚡ **你可以在接下来5分钟内做的事** +- [ ] 打开浏览器开发者工具,在碰撞检测函数设置断点 +- [ ] 尝试调整激光速度或敌人移动,观察碰撞效果 +- [ ] 实验不同的冷却时间以测试开火速率 +- [ ] 添加 `console.log` 语句以实时跟踪碰撞事件 + +### 🎯 **你这一小时可以完成的目标** +- [ ] 完成课后测验并理解碰撞检测算法 +- [ ] 添加碰撞时的视觉特效,如爆炸效果 +- [ ] 实现具有不同属性的各种类型投射物 +- [ ] 创建可暂时增强玩家能力的强化道具 +- [ ] 添加碰撞音效,使体验更具满足感 + +### 📅 **你的周长物理编程计划** +- [ ] 完成带有完善碰撞系统的完整太空游戏 +- [ ] 实现矩形之外的高级碰撞形状(圆形、多边形) +- [ ] 添加粒子系统以实现逼真的爆炸效果 +- [ ] 创建具有碰撞规避的复杂敌人行为 +- [ ] 优化碰撞检测以支持大量对象的高性能运行 +- [ ] 添加动量和真实运动的物理模拟 + +### 🌟 **你的月度游戏物理精通计划** +- [ ] 构建使用高级物理引擎和真实模拟的游戏 +- [ ] 学习3D碰撞检测和空间划分算法 +- [ ] 为开源物理库和游戏引擎做出贡献 +- [ ] 掌握图形密集型应用的性能优化 +- [ ] 创建关于游戏物理和碰撞检测的教育内容 +- [ ] 构建展示高级物理编程技能的作品集 + +## 🎯 你的碰撞检测精通时间轴 + +```mermaid +timeline + title 碰撞检测与游戏物理学习进度 + + section 基础(10分钟) + Rectangle Math: 坐标系 + : 边界计算 + : 位置跟踪 + : 尺寸管理 + + section 算法设计(20分钟) + Intersection Logic: 分离测试 + : 重叠检测 + : 性能优化 + : 边界情况处理 + + section 游戏实现(30分钟) + Object Systems: 生命周期管理 + : 事件协调 + : 状态跟踪 + : 内存清理 + + section 交互功能(40分钟) + Combat Mechanics: 投射物系统 + : 武器冷却 + : 伤害计算 + : 视觉反馈 + + section 高级物理(50分钟) + Real-time Systems: 帧率优化 + : 空间划分 + : 碰撞响应 + : 物理模拟 + + section 专业技巧(一周) + Game Engine Concepts: 组件系统 + : 物理流程 + : 性能分析 + : 跨平台优化 + + section 行业应用(一月) + Production Skills: 大规模优化 + : 团队协作 + : 引擎开发 + : 平台部署 +``` +### 🛠️ 你的游戏物理工具包总结 + +完成本课后,你已掌握: +- **碰撞数学**:矩形交叉算法和坐标系统 +- **性能优化**:适用于实时应用的高效碰撞检测 +- **对象生命周期管理**:安全的创建、更新与销毁模式 +- **事件驱动架构**:解耦的碰撞响应系统 +- **游戏循环集成**:基于帧的物理更新和渲染协调 +- **输入系统**:响应式控制,带频率限制和反馈 +- **内存管理**:高效的对象池和清理策略 + +**现实应用场景**:你的碰撞检测技能直接适用于: +- **交互式模拟**:科学建模与教育工具 +- **用户界面设计**:拖放交互和触摸检测 +- **数据可视化**:交互图表和可点击元素 +- **移动开发**:触控手势识别和碰撞处理 +- **机器人编程**:路径规划和障碍规避 +- **计算机图形学**:光线追踪和空间算法 + +**职业技能提升**:你现在能够: +- **设计** 实时碰撞检测的高效算法 +- **实现** 能适应对象复杂度的物理系统 +- **调试** 复杂交互系统,应用数学原理 +- **优化** 适配不同硬件和浏览器性能 +- **架构** 使用成熟设计模式构建可维护游戏系统 + +**掌握的游戏开发概念**: +- **物理模拟**:实时碰撞检测与响应 +- **性能工程**:交互应用的优化算法 +- **事件系统**:游戏组件间解耦通信 +- **对象管理**:动态内容的高效生命周期模式 +- **输入处理**:带反馈的响应式控制 + +**下一阶段**:你已准备好探索如 Matter.js 等高级物理引擎,实现3D碰撞检测,或构建复杂粒子系统! -到此为止,你的游戏已经具备了一些功能!你可以使用箭头键导航,用空格键发射激光,并且击中敌人时敌人会消失。干得好! +🌟 **成就解锁**:你已构建了具备专业级碰撞检测的完整物理交互系统! + +## GitHub Copilot Agent 挑战 🚀 + +使用 Agent 模式完成以下挑战: + +**描述:** 通过实现随机生成的强化道具提升碰撞检测系统,当英雄飞船收集时可提供临时能力。 + +**提示:** 创建一个继承于 GameObject 的 PowerUp 类,实现英雄与强化道具之间的碰撞检测。至少添加两种强化道具:一种提高射速(减少冷却时间),另一种产生临时护盾。包含在随机时间和位置生成强化道具的逻辑。 --- + + ## 🚀 挑战 -添加爆炸效果!查看[太空艺术资源库](../../../../6-space-game/solution/spaceArt/readme.txt)中的游戏资源,尝试在激光击中外星人时添加爆炸效果。 +添加一个爆炸效果!查看[Space Art 资源库](../../../../6-space-game/solution/spaceArt/readme.txt)中的游戏素材,尝试在激光击中外星人时添加爆炸效果。 ## 课后测验 @@ -299,7 +762,7 @@ npm start ## 复习与自学 -尝试调整游戏中的时间间隔,观察会发生什么变化?阅读更多关于[JavaScript 定时事件](https://www.freecodecamp.org/news/javascript-timing-events-settimeout-and-setinterval/)的内容。 +尝试调整游戏中目前的间隔时间。改变后会发生什么变化?了解更多关于[JavaScript 定时事件](https://www.freecodecamp.org/news/javascript-timing-events-settimeout-and-setinterval/)的内容。 ## 作业 @@ -307,5 +770,7 @@ npm start --- + **免责声明**: -本文档使用AI翻译服务[Co-op Translator](https://github.com/Azure/co-op-translator)进行翻译。尽管我们努力确保准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。对于因使用本翻译而引起的任何误解或误读,我们概不负责。 \ No newline at end of file +本文件由 AI 翻译服务 [Co-op Translator](https://github.com/Azure/co-op-translator) 翻译。虽然我们努力保证准确性,但请注意,自动翻译可能存在错误或不准确之处。原始文档的原语言版本应被视为权威来源。对于重要信息,建议采用专业人工翻译。因使用本翻译而产生的任何误解或误释,我们概不负责。 + \ No newline at end of file diff --git a/translations/zh/6-space-game/4-collision-detection/assignment.md b/translations/zh/6-space-game/4-collision-detection/assignment.md index 23da57b36..88b100a12 100644 --- a/translations/zh/6-space-game/4-collision-detection/assignment.md +++ b/translations/zh/6-space-game/4-collision-detection/assignment.md @@ -1,23 +1,64 @@ # 探索碰撞 -## 指导 +## 说明 -为了更好地理解碰撞的工作原理,制作一个包含少量碰撞物的小型游戏。通过按键或鼠标点击让它们移动,并在其中一个物体被撞击时触发某些事件。可以是类似陨石撞击地球,或者碰碰车的场景。发挥你的创造力! +通过创建一个自定义迷你游戏来应用你的碰撞检测知识,展示不同类型的对象交互。此作业将通过富有创意的实现和实验,帮助你理解碰撞机制。 + +### 项目要求 + +**创建一个小型互动游戏,特点包括:** +- **多个移动对象**,可以通过键盘或鼠标输入进行控制 +- **使用课程中的矩形相交原理实现的碰撞检测系统** +- **发生碰撞时的视觉反馈**(如对象销毁、颜色变化、特效) +- **让碰撞有意义且引人入胜的游戏规则** + +### 创意建议 + +**考虑实现以下场景之一:** +- **小行星带**:驾驶飞船穿越危险的太空碎片 +- **碰碰车**:创建一个基于物理的碰撞竞技场 +- **陨石防御**:保护地球免受陨石侵袭 +- **收集游戏**:收集物品,同时避开障碍 +- **领地控制**:竞争对象争夺空间 + +### 技术实现 + +**你的解决方案应展示:** +- 矩形碰撞检测的正确使用 +- 基于事件的用户输入编程 +- 对象生命周期管理(创建与销毁) +- 干净的代码组织和适当的类结构 + +### 额外挑战 + +**为你的游戏增加以下功能:** +- **碰撞时的粒子特效** +- **不同碰撞类型的音效** +- **基于碰撞结果的得分系统** +- **具有不同行为的多种碰撞类型** +- **随时间增加的渐进式难度** ## 评分标准 -| 标准 | 卓越表现 | 合格表现 | 需要改进 | -| -------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------ | ----------------- | -| | 提供完整的可运行代码示例,物体绘制到画布上,发生基本碰撞,并触发反应 | 代码在某些方面不完整 | 代码运行异常 | +| 标准 | 优秀 | 合格 | 需改进 | +|----------|-----------|----------|-------------------| +| **碰撞检测** | 实现了基于矩形的精准碰撞检测,支持多种对象类型和复杂交互规则 | 基本的碰撞检测正常,简单对象交互正确 | 碰撞检测存在问题或不稳定 | +| **代码质量** | 代码干净且组织良好,具有适当的类结构、有意义的变量名和适当注释 | 代码可用,但组织或文档有待改进 | 代码难以理解或结构差 | +| **用户交互** | 响应灵敏,玩法流畅,视觉反馈明确,机制吸引人 | 基本控制可用,反馈足够 | 控制响应迟钝或令人困惑 | +| **创意** | 原创概念,具独特功能、视觉精致和创新的碰撞行为 | 标准实现,有一些创意元素 | 基本功能,无创意增强 | + +--- -**免责声明**: -本文档使用AI翻译服务 [Co-op Translator](https://github.com/Azure/co-op-translator) 进行翻译。尽管我们努力确保翻译的准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。因使用本翻译而引起的任何误解或误读,我们概不负责。 \ No newline at end of file + +**免责声明**: +本文件使用AI翻译服务[Co-op Translator](https://github.com/Azure/co-op-translator)进行翻译。尽管我们力求准确,但请注意,自动翻译可能存在错误或不准确之处。原始文件的母语版本应被视为权威来源。对于重要信息,建议使用专业人工翻译。因使用本翻译而引起的任何误解或误释,我们不承担任何责任。 + \ No newline at end of file diff --git a/translations/zh/6-space-game/5-keeping-score/README.md b/translations/zh/6-space-game/5-keeping-score/README.md index 843e85ea8..efed58ff4 100644 --- a/translations/zh/6-space-game/5-keeping-score/README.md +++ b/translations/zh/6-space-game/5-keeping-score/README.md @@ -1,23 +1,89 @@ -# 构建太空游戏第五部分:得分和生命 - +# 构建太空游戏 第5部分:计分与生命 + +```mermaid +journey + title 你的游戏设计之旅 + section 玩家反馈 + 了解评分心理学: 3: Student + 学习视觉传达: 4: Student + 设计奖励系统: 4: Student + section 技术实现 + 画布文本渲染: 4: Student + 状态管理: 5: Student + 事件驱动更新: 5: Student + section 游戏润色 + 用户体验设计: 5: Student + 平衡挑战与奖励: 5: Student + 创建引人入胜的游戏玩法: 5: Student +``` ## 课前测验 [课前测验](https://ff-quizzes.netlify.app/web/quiz/37) -在本课中,你将学习如何为游戏添加得分功能以及计算生命值。 +准备让你的太空游戏感觉像一个真正的游戏了吗?让我们添加计分和生命管理——这些核心机制将早期街机游戏如《Space Invaders》从简单演示转变为让人上瘾的娱乐。这是你游戏真正可玩起来的关键。 + +```mermaid +mindmap + root((游戏反馈系统)) + Visual Communication + Text Rendering + Icon Display + Color Psychology + Layout Design + Scoring Mechanics + Point Values + Reward Timing + Progress Tracking + Achievement Systems + Life Management + Risk vs Reward + Player Agency + Difficulty Balance + Recovery Mechanics + User Experience + Immediate Feedback + Clear Information + Emotional Response + Engagement Loops + Implementation + Canvas API + State Management + Event Systems + Performance +``` +## 在屏幕上绘制文本——游戏的声音 -## 在屏幕上绘制文本 +要显示你的分数,我们需要学习如何在画布上渲染文字。`fillText()` 方法是你的主要工具——这也是经典街机游戏用来显示分数和状态信息的技术。 -为了能够在屏幕上显示游戏得分,你需要知道如何在屏幕上放置文本。答案是使用画布对象的 `fillText()` 方法。你还可以控制其他方面,比如使用什么字体、文本的颜色,甚至文本的对齐方式(左对齐、右对齐、居中)。下面是一些代码示例,用于在屏幕上绘制文本。 +```mermaid +flowchart LR + A["📝 文本内容"] --> B["🎨 样式"] + B --> C["📍 位置"] + C --> D["🖼️ 画布渲染"] + + E["字体"] --> B + F["字号"] --> B + G["颜色"] --> B + H["对齐"] --> B + + I["X 坐标"] --> C + J["Y 坐标"] --> C + + style A fill:#e3f2fd + style B fill:#e8f5e8 + style C fill:#fff3e0 + style D fill:#f3e5f5 +``` +你可以完全控制文本的外观: ```javascript ctx.font = "30px Arial"; @@ -26,22 +92,74 @@ ctx.textAlign = "right"; ctx.fillText("show this on the screen", 0, 0); ``` -✅ 阅读更多关于[如何在画布上添加文本](https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Drawing_text)的信息,并随意让你的文本看起来更有趣! +✅ 深入了解 [向画布添加文本](https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Drawing_text) ——你可能会惊讶于字体和样式可以多么富有创造性! -## 生命值,作为游戏概念 +## 生命值——不仅仅是一个数字 -在游戏中,生命值的概念只是一个数字。在太空游戏的背景下,通常会分配一组生命值,当你的飞船受到伤害时,生命值会逐个减少。如果能用图形化的方式展示生命值,比如小型飞船或心形图标,而不是简单的数字,会更直观。 +在游戏设计中,“生命”代表玩家的容错空间。这个概念可以追溯到弹珠机时代,你会获得多个球来游戏。在早期视频游戏,如《Asteroids》中,生命给玩家提供了冒险和从错误中学习的许可。 -## 要构建的内容 +```mermaid +flowchart TD + A["🎮 玩家行动"] --> B{"风险评估"} + + B --> C["高风险,高回报"] + B --> D["安全策略"] + + C --> E{"结果"} + D --> F["稳步进展"] + + E -->|成功| G["🏆 大分数"] + E -->|失败| H["💔 失去生命"] + + H --> I{"剩余生命?"} + I -->|是| J["🔄 再试一次"] + I -->|否| K["💀 游戏结束"] + + J --> B + G --> B + F --> B + + style C fill:#ffebee + style D fill:#e8f5e8 + style G fill:#e3f2fd + style H fill:#fff3e0 +``` +视觉表现尤为重要——展示飞船图标而不是简单的“生命:3”可以立即产生视觉识别,就像早期街机机台用图标来跨越语言障碍传达信息一样。 -让我们为你的游戏添加以下内容: +## 构建游戏的奖励系统 -- **游戏得分**:每摧毁一艘敌方飞船,英雄应该获得一些分数,我们建议每艘飞船奖励100分。游戏得分应显示在屏幕左下角。 -- **生命值**:你的飞船有三条生命。每当敌方飞船与你碰撞时,你会失去一条生命。生命值应显示在屏幕右下角,并由以下图形表示 ![生命图标](../../../../translated_images/life.6fb9f50d53ee0413cd91aa411f7c296e10a1a6de5c4a4197c718b49bf7d63ebf.zh.png)。 +现在我们将实现保持玩家投入的核心反馈系统: + +```mermaid +sequenceDiagram + participant Player + participant GameEngine + participant ScoreSystem + participant LifeSystem + participant Display + + Player->>GameEngine: 射击敌人 + GameEngine->>ScoreSystem: 奖励积分 + ScoreSystem->>ScoreSystem: +100 分 + ScoreSystem->>Display: 更新分数 + + Player->>GameEngine: 与敌人碰撞 + GameEngine->>LifeSystem: 失去生命 + LifeSystem->>LifeSystem: -1 生命 + LifeSystem->>Display: 更新生命值 + + alt 生命值 > 0 + LifeSystem->>Player: 继续游戏 + else 生命值 = 0 + LifeSystem->>GameEngine: 游戏结束 + end +``` +- **计分系统**:每摧毁一艘敌舰奖励100分(圆整数字更便于玩家心算)。分数显示在左下角。 +- **生命计数器**:你的英雄从三条命开始——这是早期街机游戏确立的标准,平衡挑战和可玩性。每次与敌人碰撞会损失一条命。我们将在右下用飞船图标显示剩余生命 ![life image](../../../../translated_images/life.6fb9f50d53ee0413.zh.png)。 -## 推荐步骤 +## 开始构建吧! -找到在 `your-work` 子文件夹中为你创建的文件。它应该包含以下内容: +首先,设置你的工作区。进入 `your-work` 子文件夹中的文件。你应该能看到这些文件: ```bash -| assets @@ -53,24 +171,49 @@ ctx.fillText("show this on the screen", 0, 0); -| package.json ``` -通过输入以下命令启动你的项目: +要测试你的游戏,从 `your_work` 文件夹启动开发服务器: ```bash cd your-work npm start ``` -上述命令将在地址 `http://localhost:5000` 上启动一个 HTTP 服务器。打开浏览器并输入该地址,现在它应该渲染英雄和所有敌人,并且当你按下左右箭头时,英雄会移动并可以击落敌人。 +这会在 `http://localhost:5000` 运行本地服务器。打开这个地址查看你的游戏。用箭头键测试控制,尝试射击敌人以验证一切正常。 -### 添加代码 +```mermaid +flowchart TD + A["1. 资源加载"] --> B["2. 游戏变量"] + B --> C["3. 碰撞检测"] + C --> D["4. 英雄强化"] + D --> E["5. 显示功能"] + E --> F["6. 事件处理器"] + + G["生命图标图片"] --> A + H["得分与生命追踪"] --> B + I["英雄与敌人交集"] --> C + J["积分与生命方法"] --> D + K["文本与图标渲染"] --> E + L["奖励与惩罚逻辑"] --> F + + F --> M["🎮 完整游戏"] + + style A fill:#e3f2fd + style B fill:#e8f5e8 + style C fill:#fff3e0 + style D fill:#f3e5f5 + style E fill:#e0f2f1 + style F fill:#fce4ec + style M fill:#e1f5fe +``` +### 开始编码! -1. **复制所需资源** 从 `solution/assets/` 文件夹复制到 `your-work` 文件夹;你需要添加一个 `life.png` 资源。在 `window.onload` 函数中添加 lifeImg: +1. **获取所需的视觉素材**。将 `solution/assets/` 文件夹里的 `life.png` 复制到你的 `your-work` 文件夹。然后将 lifeImg 添加到你的 window.onload 函数中: ```javascript lifeImg = await loadTexture("assets/life.png"); ``` -1. 将 `lifeImg` 添加到资源列表中: +1. 别忘了将 `lifeImg` 添加到你的素材列表中: ```javascript let heroImg, @@ -80,9 +223,9 @@ npm start eventEmitter = new EventEmitter(); ``` -2. **添加变量**。添加代码以表示你的总得分(初始为0)和剩余生命值(初始为3),并在屏幕上显示这些分数。 +2. **设置游戏变量**。添加代码以跟踪总分(从0开始)和剩余生命(从3开始)。我们将在屏幕上显示这些信息,让玩家随时了解自己的状态。 -3. **扩展 `updateGameObjects()` 函数**。扩展 `updateGameObjects()` 函数以处理敌人碰撞: +3. **实现碰撞检测**。扩展你的 `updateGameObjects()` 函数以检测敌人与英雄的碰撞: ```javascript enemies.forEach(enemy => { @@ -93,19 +236,19 @@ npm start }) ``` -4. **添加生命值和得分**。 - 1. **初始化变量**。在 `Hero` 类中的 `this.cooldown = 0` 下设置生命值和得分: +4. **为英雄添加生命和积分追踪**。 + 1. **初始化计数器**。在 `Hero` 类中 `this.cooldown = 0` 下,设置生命和积分: ```javascript this.life = 3; this.points = 0; ``` - 1. **在屏幕上绘制变量**。将这些值绘制到屏幕上: + 1. **向玩家显示这些数值**。创建函数以在屏幕上绘制这些数值: ```javascript function drawLife() { - // TODO, 35, 27 + // 待办事项,35,27 const START_POS = canvas.width - 180; for(let i=0; i < hero.life; i++ ) { ctx.drawImage( @@ -128,18 +271,34 @@ npm start ``` - 1. **将方法添加到游戏循环中**。确保在 `window.onload` 函数中将这些函数添加到 `updateGameObjects()` 下: + 1. **将所有内容挂钩到你的游戏循环中**。在 window.onload 函数中 `updateGameObjects()` 之后调用这些函数: ```javascript drawPoints(); drawLife(); ``` -1. **实现游戏规则**。实现以下游戏规则: +### 🔄 **教学检查点** +**游戏设计理解**:在实施后果之前,确保你理解: +- ✅ 视觉反馈如何向玩家传递游戏状态 +- ✅ UI 元素一致放置为何提升可用性 +- ✅ 分值和生命管理背后的心理学 +- ✅ 画布文字渲染与 HTML 文字的不同 + +**快速自测**:为何街机游戏通常使用圆整的点数? +*答案:圆整数字便于玩家心算,且带来心理上的满足感* + +**用户体验原则**:你正在应用: +- **视觉层级**:重要信息醒目展示 +- **即时反馈**:玩家操作即时更新 +- **认知负荷**:信息简洁明了呈现 +- **情感设计**:图标和颜色营造玩家连接感 + +1. **实现游戏后果与奖励**。现在我们添加让玩家行为有意义的反馈系统: - 1. **每次英雄与敌人碰撞**,扣除一条生命。 + 1. **碰撞消耗生命**。每次英雄撞上敌人,你应该失去一条命。 - 扩展 `Hero` 类以实现生命值扣除: + 将此方法添加到 `Hero` 类中: ```javascript decrementLife() { @@ -150,17 +309,17 @@ npm start } ``` - 2. **每次激光击中敌人**,游戏得分增加100分。 + 2. **射击敌人得分**。每命中一次奖励100分,为精准射击提供即时正反馈。 + + 在 Hero 类中扩展此增加函数: - 扩展 `Hero` 类以实现得分增加: - ```javascript incrementPoints() { this.points += 100; } ``` - 将这些函数添加到碰撞事件发射器中: + 现将这些函数与碰撞事件关联: ```javascript eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => { @@ -175,15 +334,159 @@ npm start }); ``` -✅ 进行一些研究,了解其他使用 JavaScript/Canvas 创建的游戏。它们有哪些共同特点? - -完成这部分工作后,你应该能在屏幕右下角看到小型“生命”飞船,左下角看到得分,并且当你与敌人碰撞时生命值会减少,当你击中敌人时得分会增加。干得好!你的游戏已经接近完成。 +✅ 想了解更多用 JavaScript 和 Canvas 构建的游戏?多做探索——你可能会惊讶于其可能性! + +实现这些功能后,测试你的游戏,观察完整反馈系统的运行。你应该能看到右下的生命图标,左下的分数,并见证碰撞减少生命、成功击中增加分数。 + +你的游戏现在拥有了早期街机游戏令人着迷的核心机制——明确目标、即时反馈和玩家操作的有意义后果。 + +### 🔄 **教学检查点** +**完整游戏设计系统**:检验你对玩家反馈系统的掌握: +- ✅ 计分机制如何激发玩家动力和投入? +- ✅ 视觉一致性为何对用户界面设计重要? +- ✅ 生命系统如何平衡挑战与玩家留存? +- ✅ 即时反馈在创造令人满意的游戏体验中起什么作用? + +**系统集成**:你的反馈系统体现了: +- **用户体验设计**:清晰视觉传达和信息层级 +- **事件驱动架构**:对玩家操作的响应更新 +- **状态管理**:追踪并显示动态游戏数据 +- **画布掌握**:文字渲染和精灵定位 +- **游戏心理学**:理解玩家动力和投入 + +**专业模式**:你已实现: +- **MVC 架构**:游戏逻辑、数据和表现分离 +- **观察者模式**:游戏状态变更的事件驱动更新 +- **组件设计**:可复用的渲染和逻辑函数 +- **性能优化**:游戏循环中的高效渲染 + +### ⚡ **未来5分钟可做的事** +- [ ] 试验不同的字体大小和颜色显示分数 +- [ ] 改变点数值,观察对游戏感觉的影响 +- [ ] 添加 console.log 跟踪分数和生命的变动 +- [ ] 测试边缘情况,如生命耗尽或高分达成 + +### 🎯 **本小时可完成的目标** +- [ ] 完成课后测验并理解游戏设计心理学 +- [ ] 增加得分和失生命的音效 +- [ ] 利用 localStorage 实现高分系统 +- [ ] 为不同敌人类型设置不同点数 +- [ ] 添加失去生命时的屏幕震动视觉效果 + +### 📅 **你的周长游戏设计之旅** +- [ ] 完成具有完善反馈系统的完整太空游戏 +- [ ] 实现高级计分机制如连击倍率 +- [ ] 增加成就和可解锁内容 +- [ ] 创建难度递增和平衡系统 +- [ ] 设计菜单和游戏结束界面 +- [ ] 学习其他游戏以理解用户投入机制 + +### 🌟 **你的月长游戏开发精通** +- [ ] 构建配有复杂进程系统的完整游戏 +- [ ] 学习游戏分析与玩家行为测量 +- [ ] 贡献开源游戏开发项目 +- [ ] 掌握高级游戏设计模式和变现策略 +- [ ] 制作关于游戏设计和用户体验的教育内容 +- [ ] 建立展示游戏设计与开发技能的作品集 + +## 🎯 你的游戏设计精通时间线 + +```mermaid +timeline + title 游戏设计与玩家反馈学习进程 + + section 基础(10分钟) + 视觉传达:文本渲染 + :图标设计 + :布局原则 + :色彩心理学 + + section 玩家心理(20分钟) + 动机系统:积分值 + :风险与回报 + :进度反馈 + :成就设计 + + section 技术实现(30分钟) + 画布精通:文本定位 + :精灵渲染 + :状态管理 + :性能优化 + + section 游戏平衡(40分钟) + 难度设计:生命管理 + :评分曲线 + :玩家留存 + :无障碍设计 + + section 用户体验(50分钟) + 界面设计:信息层级 + :响应反馈 + :情感设计 + :可用性测试 + + section 高级系统(一周) + 游戏机制:进程系统 + :分析集成 + :变现设计 + :社区功能 + + section 行业技能(一月) + 职业发展:团队协作 + :设计文档 + :玩家研究 + :平台优化 +``` +### 🛠️ 你的游戏设计工具包总结 + +完成本课后,你已掌握: +- **玩家心理学**:理解动机、风险/奖励与投入循环 +- **视觉传达**:使用文本、图标和布局进行有效UI设计 +- **反馈系统**:实时响应玩家动作和游戏事件 +- **状态管理**:高效追踪及展示动态游戏数据 +- **画布文本渲染**:专业文字展示及样式定位 +- **事件整合**:将用户操作连接至有意义的游戏后果 +- **游戏平衡**:设计难度曲线和玩家进程系统 + +**现实应用**:你的游戏设计技能直接适用于: +- **用户界面设计**:创造引人入胜、直观界面 +- **产品开发**:理解用户动机和反馈循环 +- **教育技术**:游戏化和学习投入系统 +- **数据可视化**:让复杂信息易于理解和吸引人 +- **移动应用开发**:留存机制和用户体验设计 +- **市场技术**:理解用户行为和转化优化 + +**获得的专业技能**: +- **设计** 激励和吸引用户的用户体验 +- **实现** 指导用户行为的反馈系统 +- **平衡** 互动系统的挑战与可达性 +- **创建** 跨用户群有效的视觉传达 +- **分析** 用户行为并迭代设计改进 + +**掌握的游戏开发概念**: +- **玩家动机**:理解驱动投入与留存的因素 +- **视觉设计**:创建清晰、美观且实用的界面 +- **系统集成**:多系统连接以实现一致体验 +- **性能优化**:高效渲染与状态管理 +- **无障碍设计**:面向不同技能水平和玩家需求设计 + +**下一步**:你已准备探索高级游戏设计模式,实施分析系统,或研究游戏变现和玩家留存策略! + +🌟 **成就达成**:你已经构建了一个带有专业游戏设计原则的完整玩家反馈系统! --- +## GitHub Copilot Agent 挑战 🚀 + +使用代理模式完成以下挑战: + +**描述:** 增强太空游戏的计分系统,实现带持久存储的最高分功能和连击奖励机制。 + +**提示:** 创建一个最高分系统,将玩家最高分保存到 localStorage。新增连击得分奖励(连杀系统),并针对不同敌人类型设定不同点值。新增视觉指示,提示玩家刷新最高分,并在游戏画面显示当前最高分。 + ## 🚀 挑战 -你的代码几乎完成了。你能想象下一步该做什么吗? +你现在拥有一个功能齐全的带计分和生命的游戏。思考还有哪些额外功能可以提升玩家体验。 ## 课后测验 @@ -191,13 +494,15 @@ npm start ## 复习与自学 -研究一些可以增加和减少游戏得分和生命值的方法。有一些有趣的游戏引擎,比如 [PlayFab](https://playfab.com)。使用这些引擎如何提升你的游戏? +想深入探索?研究不同的计分和生命系统方法。有许多有趣的游戏引擎,比如 [PlayFab](https://playfab.com),它们管理计分、排行榜和玩家进程。集成类似功能将怎样提升你的游戏? ## 作业 -[构建一个得分游戏](assignment.md) +[构建一个计分游戏](assignment.md) --- + **免责声明**: -本文档使用AI翻译服务[Co-op Translator](https://github.com/Azure/co-op-translator)进行翻译。尽管我们努力确保准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。因使用本翻译而引起的任何误解或误读,我们概不负责。 \ No newline at end of file +本文件由人工智能翻译服务 [Co-op Translator](https://github.com/Azure/co-op-translator) 翻译。尽管我们力求准确,但请注意自动翻译可能存在错误或不准确之处。原始文档的母语版本应视为权威来源。对于重要信息,建议采用专业人工翻译。因使用本翻译而产生的任何误解或曲解,我们不承担任何责任。 + \ No newline at end of file diff --git a/translations/zh/6-space-game/6-end-condition/README.md b/translations/zh/6-space-game/6-end-condition/README.md index 9ad725e1f..de4dc33e1 100644 --- a/translations/zh/6-space-game/6-end-condition/README.md +++ b/translations/zh/6-space-game/6-end-condition/README.md @@ -1,41 +1,163 @@ -# 构建太空游戏第六部分:结束与重启 - +# 构建太空游戏第6部分:结束与重启 + +```mermaid +journey + title 你的游戏完成旅程 + section 结束条件 + 定义胜负状态: 3: Student + 实现条件检测: 4: Student + 处理状态转换: 4: Student + section 玩家体验 + 设计反馈系统: 4: Student + 创建重启机制: 5: Student + 优化用户界面: 5: Student + section 系统集成 + 管理游戏生命周期: 5: Student + 处理内存清理: 5: Student + 创建完整体验: 5: Student +``` +每个伟大的游戏都需要清晰的结束条件和流畅的重启机制。你已经打造了一个具有移动、战斗和计分功能的令人印象深刻的太空游戏——现在是时候添加最终的部分,让它感觉完整了。 + +你的游戏当前无限运行,就像美国宇航局1977年发射的旅行者探测器——几十年后仍在太空中旅行。虽然这对于太空探索很好,但游戏需要定义好的终点来创造令人满意的体验。 + +今天,我们将实现合适的胜负条件和重启系统。课程结束时,你将拥有一个抛光完善的游戏,玩家可以完成并重玩,就像定义了媒介的经典街机游戏一样。 + +```mermaid +mindmap + root((游戏完成)) + End Conditions + Victory States[胜利状态] + Defeat Conditions[失败条件] + Progress Tracking[进度跟踪] + State Validation[状态验证] + Player Feedback + Visual Messages[视觉信息] + Color Psychology[色彩心理学] + Clear Communication[清晰传达] + Emotional Response[情感反应] + State Management + Game Loop Control[游戏循环控制] + Memory Cleanup[内存清理] + Object Lifecycle[对象生命周期] + Event Handling[事件处理] + Restart Systems + Input Handling[输入处理] + State Reset[状态重置] + Fresh Initialization[全新初始化] + User Experience[用户体验] + Polish Elements + Message Display[消息显示] + Smooth Transitions[平滑过渡] + Error Prevention[错误预防] + Accessibility[无障碍] +``` ## 课前测验 [课前测验](https://ff-quizzes.netlify.app/web/quiz/39) -在游戏中,有多种方式来表达*结束条件*。作为游戏的创作者,你需要决定游戏为何结束。以下是一些可能的原因,假设我们正在讨论你目前正在构建的太空游戏: +## 理解游戏结束条件 -- **摧毁了`N`艘敌方飞船**:如果你将游戏分为不同的关卡,那么通常需要摧毁`N`艘敌方飞船才能完成一个关卡。 -- **你的飞船被摧毁**:有些游戏中,如果你的飞船被摧毁,你就会输掉游戏。另一种常见的方法是引入“生命”的概念。每次飞船被摧毁时,扣除一条生命。当所有生命耗尽时,游戏结束。 -- **收集了`N`分**:另一种常见的结束条件是收集一定的分数。如何获得分数取决于你,但通常会为各种活动分配分数,比如摧毁敌方飞船,或者收集敌方飞船被摧毁后掉落的物品。 -- **完成一个关卡**:这可能涉及多个条件,比如摧毁`X`艘敌方飞船、收集`Y`分,或者收集某个特定的物品。 +你的游戏应该何时结束?这个基础问题自早期街机时代以来影响了游戏设计。吃豆人被鬼抓住或吃掉所有豆子即结束,太空入侵者则是在外星人到达底部或全部被击毁时结束。 -## 重启 +作为游戏创作者,你定义胜利和失败条件。对于我们的太空游戏,以下是创造引人入胜玩法的常见方式: -如果玩家喜欢你的游戏,他们可能会想要重新玩一次。当游戏因某种原因结束时,你应该提供一个重启的选项。 +```mermaid +flowchart TD + A["🎮 游戏开始"] --> B{"检查条件"} + + B --> C["敌人数"] + B --> D["英雄生命"] + B --> E["得分阈值"] + B --> F["关卡进度"] + + C --> C1{"敌人 = 0?"} + D --> D1{"生命 = 0?"} + E --> E1{"得分 ≥ 目标?"} + F --> F1{"目标完成?"} + + C1 -->|是| G["🏆 胜利"] + D1 -->|是| H["💀 失败"] + E1 -->|是| G + F1 -->|是| G + + C1 -->|否| B + D1 -->|否| B + E1 -->|否| B + F1 -->|否| B + + G --> I["🔄 重新开始选项"] + H --> I + + style G fill:#e8f5e8 + style H fill:#ffebee + style I fill:#e3f2fd +``` +- **已击毁 `N` 艘敌舰**:如果你将游戏分为不同关卡,通常需要击毁 `N` 艘敌舰以完成关卡 +- **你的飞船被击毁**:有些游戏中,如果你的船被毁你就会输。一种常见方式是生命值机制。每当你的船被毁损失一条命,命数耗尽时游戏失败。 +- **你获得了 `N` 分**:另一种常见的结束条件是你收集到足够的分数。分数来源由你决定,通常分数赋予消灭敌舰或收集敌舰被击毁后掉落的物品。 +- **完成一个关卡**:这可能涉及多个条件,如击毁 `X` 艘敌舰、收集 `Y` 分数,或收集特定道具。 -✅ 想一想,在什么条件下你认为游戏会结束,然后玩家会如何被提示重启。 +## 实现游戏重启功能 -## 要构建的内容 +好的游戏通过流畅的重启机制提高重玩价值。当玩家完成游戏(或失败)时,通常希望立即再试一次——无论是为了超越分数还是提升表现。 -你需要为游戏添加以下规则: +```mermaid +stateDiagram-v2 + [*] --> Playing: 游戏开始 + Playing --> Victory: 消灭所有敌人 + Playing --> Defeat: 生命值 = 0 + + Victory --> MessageDisplay: 显示胜利信息 + Defeat --> MessageDisplay: 显示失败信息 + + MessageDisplay --> WaitingRestart: 按回车提示 + WaitingRestart --> Resetting: 按下回车键 + + Resetting --> CleanupMemory: 清除定时器 + CleanupMemory --> ClearEvents: 移除监听器 + ClearEvents --> InitializeGame: 全新开始 + InitializeGame --> Playing: 新游戏开始 + + note right of MessageDisplay + 颜色编码反馈: + 绿色 = 胜利 + 红色 = 失败 + end note + + note right of Resetting + 完成状态重置 + 防止内存泄漏 + end note +``` +俄罗斯方块是这方面的典范:当方块堆满顶部时,你可以立即开始新游戏,无需复杂菜单操作。我们将构建类似的重启系统,清晰地重置游戏状态,让玩家迅速回归游戏。 + +✅ **思考**:想想你玩过的游戏。它们在什么情况下结束?如何提示你重启?什么样的重启体验让人感觉顺畅而非烦躁? + +## 你将构建的内容 + +你将实现最终的功能,把项目打造成完整的游戏体验。这些元素让精致游戏区别于基础原型。 + +**今天我们添加的内容:** + +1. **胜利条件**:消灭全部敌人并庆祝胜利(你应得的!) +2. **失败条件**:耗尽所有生命,面对失败画面 +3. **重启机制**:按回车键即可重新开始——因为一场游戏永远不够 +4. **状态管理**:每次都是崭新开始——无遗留敌人或奇怪故障 -1. **赢得游戏**。当所有敌方飞船被摧毁时,玩家赢得游戏。此外,显示某种胜利信息。 -2. **重启**。当所有生命耗尽或游戏胜利时,你应该提供一种方式来重启游戏。记住!你需要重新初始化游戏,并清除之前的游戏状态。 +## 准备工作 -## 推荐步骤 +让我们准备开发环境。你应该已经有上节课的所有太空游戏文件。 -找到在`your-work`子文件夹中为你创建的文件。它应该包含以下内容: +**你的项目文件结构应大致如下:** ```bash -| assets @@ -48,175 +170,499 @@ CO_OP_TRANSLATOR_METADATA: -| package.json ``` -通过输入以下命令启动你的项目: +**启动你的开发服务器:** ```bash cd your-work npm start ``` -上述命令将在地址`http://localhost:5000`上启动一个HTTP服务器。打开浏览器并输入该地址。你的游戏应该处于可玩的状态。 +**该命令执行:** +- 在 `http://localhost:5000` 运行本地服务器 +- 正确提供你的文件服务 +- 当你修改时自动刷新浏览器 -> 提示:为了避免在Visual Studio Code中出现警告,编辑`window.onload`函数,使其直接调用`gameLoopId`(不使用`let`),并在文件顶部独立声明`gameLoopId`:`let gameLoopId;` +在浏览器打开 `http://localhost:5000` 并验证游戏是否能运行。你应该可以移动,发射并与敌人互动。确认后,我们即可开始实现功能。 + +> 💡 **专业建议**:为避免Visual Studio Code中警告,请在文件顶部声明 `let gameLoopId;`,而不是在 `window.onload` 函数内声明。这符合现代JavaScript变量声明最佳实践。 + +```mermaid +flowchart TD + A["1. 条件追踪"] --> B["2. 事件处理器"] + B --> C["3. 消息常量"] + C --> D["4. 重启控制"] + D --> E["5. 消息显示"] + E --> F["6. 重置系统"] + + G["isHeroDead()\nisEnemiesDead()"] --> A + H["碰撞事件\n结束游戏事件"] --> B + I["游戏结束_胜利\n游戏结束_失败"] --> C + J["回车键\n重启触发"] --> D + K["胜利/失败\n颜色编码文本"] --> E + L["状态清理\n全新初始化"] --> F + + F --> M["🎮 完整游戏"] + + style A fill:#e3f2fd + style B fill:#e8f5e8 + style C fill:#fff3e0 + style D fill:#f3e5f5 + style E fill:#e0f2f1 + style F fill:#fce4ec + style M fill:#e1f5fe +``` +## 实现步骤 -### 添加代码 +### 步骤1:创建结束条件追踪函数 -1. **追踪结束条件**。添加代码以追踪敌方飞船数量,或者英雄飞船是否被摧毁,方法是添加以下两个函数: +我们需要函数来监测游戏何时结束。就像国际空间站上的传感器不断监控关键系统一样,这些函数将持续检查游戏状态。 - ```javascript - function isHeroDead() { - return hero.life <= 0; +```javascript +function isHeroDead() { + return hero.life <= 0; +} + +function isEnemiesDead() { + const enemies = gameObjects.filter((go) => go.type === "Enemy" && !go.dead); + return enemies.length === 0; +} +``` + +**幕后发生的事情:** +- **检查** 主角是否已无生命(糟糕!) +- **计算** 当前还活着并在战斗的敌人数量 +- **返回** 当战场清空敌人时返回 `true` +- **使用** 简单的真假逻辑保持清晰明了 +- **过滤** 所有游戏对象,找出存活者 + +### 步骤2:更新事件处理器以检测结束条件 + +现在我们将把这些条件检测连接到游戏事件系统中。每当发生碰撞时,游戏会评估是否触发结束条件。这为关键游戏事件创建即时反馈。 + +```mermaid +sequenceDiagram + participant Collision + participant GameLogic + participant Conditions + participant EventSystem + participant Display + + Collision->>GameLogic: 激光击中敌人 + GameLogic->>GameLogic: 摧毁对象 + GameLogic->>Conditions: 检查 isEnemiesDead() + + alt 全部敌人被击败 + Conditions->>EventSystem: 发送 GAME_END_WIN + EventSystem->>Display: 显示胜利消息 + else 敌人仍然存在 + Conditions->>GameLogic: 继续游戏 + end + + Collision->>GameLogic: 敌人攻击英雄 + GameLogic->>GameLogic: 减少生命值 + GameLogic->>Conditions: 检查 isHeroDead() + + alt 生命值 = 0 + Conditions->>EventSystem: 发送 GAME_END_LOSS + EventSystem->>Display: 显示失败消息 + else 生命值仍然存在 + GameLogic->>Conditions: 检查 isEnemiesDead() + alt 全部敌人被击败 + Conditions->>EventSystem: 发送 GAME_END_WIN + end + end +``` +```javascript +eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => { + first.dead = true; + second.dead = true; + hero.incrementPoints(); + + if (isEnemiesDead()) { + eventEmitter.emit(Messages.GAME_END_WIN); } +}); + +eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => { + enemy.dead = true; + hero.decrementLife(); + if (isHeroDead()) { + eventEmitter.emit(Messages.GAME_END_LOSS); + return; // 胜利之前的代价 + } + if (isEnemiesDead()) { + eventEmitter.emit(Messages.GAME_END_WIN); + } +}); + +eventEmitter.on(Messages.GAME_END_WIN, () => { + endGame(true); +}); + +eventEmitter.on(Messages.GAME_END_LOSS, () => { + endGame(false); +}); +``` + +**这里发生的事情:** +- **激光击中敌人**:双方消失,你得分,我们检查你是否获胜 +- **敌人撞击你**:你失去一条命,我们检查你是否还活着 +- **合理顺序**:先检查失败(没人想同时赢又输!) +- **即时反应**:重要事件发生时游戏立刻感知 + +### 步骤3:添加新消息常量 + +你需要为 `Messages` 常量对象添加新的消息类型。这些常量帮助保持一致性,避免事件系统中的拼写错误。 + +```javascript +GAME_END_LOSS: "GAME_END_LOSS", +GAME_END_WIN: "GAME_END_WIN", +``` + +**上面内容:** +- **添加** 代表游戏结束事件的常量以保持一致性 +- **使用** 描述性名称清晰标识事件目的 +- **遵循** 现有消息类型命名规范 + +### 步骤4:实现重启控制 + +接下来添加键盘控制,让玩家能重启游戏。回车键是自然选择,因其常用作确认与开始新游戏的按键。 + +**在现有keydown事件监听器中添加对回车键的检测:** + +```javascript +else if(evt.key === "Enter") { + eventEmitter.emit(Messages.KEY_EVENT_ENTER); +} +``` + +**添加新的消息常量:** + +```javascript +KEY_EVENT_ENTER: "KEY_EVENT_ENTER", +``` + +**你需要知道的:** +- **扩展** 你现有的键盘事件处理系统 +- **使用** 回车键作为重启触发器,提升用户体验直观性 +- **发布** 自定义事件,供游戏的其他部分监听 +- **保持** 与其他键盘控制相同模式 + +### 步骤5:创建消息显示系统 - function isEnemiesDead() { - const enemies = gameObjects.filter((go) => go.type === "Enemy" && !go.dead); - return enemies.length === 0; +游戏需要清楚地向玩家传达结果。我们将创建一个消息系统,使用颜色编码的文本显示胜利和失败状态,类似早期计算机终端界面绿色表示成功,红色表示错误。 + +**创建 `displayMessage()` 函数:** + +```javascript +function displayMessage(message, color = "red") { + ctx.font = "30px Arial"; + ctx.fillStyle = color; + ctx.textAlign = "center"; + ctx.fillText(message, canvas.width / 2, canvas.height / 2); +} +``` + +**一步步,发生了什么:** +- **设置** 字号和字体,确保文字清晰易读 +- **应用** 颜色参数,默认为“红色”表示警告 +- **让** 文字在画布水平和垂直居中 +- **利用** 现代JavaScript默认参数支持灵活颜色选择 +- **借助** canvas 2D上下文直接渲染文本 + +**创建 `endGame()` 函数:** + +```javascript +function endGame(win) { + clearInterval(gameLoopId); + + // 设置延迟以确保所有待处理的渲染完成 + setTimeout(() => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + if (win) { + displayMessage( + "Victory!!! Pew Pew... - Press [Enter] to start a new game Captain Pew Pew", + "green" + ); + } else { + displayMessage( + "You died !!! Press [Enter] to start a new game Captain Pew Pew" + ); } - ``` - -2. **添加逻辑到消息处理程序**。编辑`eventEmitter`以处理这些条件: - - ```javascript - eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => { - first.dead = true; - second.dead = true; - hero.incrementPoints(); - - if (isEnemiesDead()) { - eventEmitter.emit(Messages.GAME_END_WIN); - } - }); - - eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => { - enemy.dead = true; - hero.decrementLife(); - if (isHeroDead()) { - eventEmitter.emit(Messages.GAME_END_LOSS); - return; // loss before victory - } - if (isEnemiesDead()) { - eventEmitter.emit(Messages.GAME_END_WIN); - } - }); - - eventEmitter.on(Messages.GAME_END_WIN, () => { - endGame(true); - }); - - eventEmitter.on(Messages.GAME_END_LOSS, () => { - endGame(false); - }); - ``` - -3. **添加新的消息类型**。将这些消息添加到常量对象中: - - ```javascript - GAME_END_LOSS: "GAME_END_LOSS", - GAME_END_WIN: "GAME_END_WIN", - ``` - -4. **添加重启代码**,以便在按下选定按钮时重启游戏。 - - 1. **监听按键`Enter`**。编辑窗口的事件监听器以监听该按键: - - ```javascript - else if(evt.key === "Enter") { - eventEmitter.emit(Messages.KEY_EVENT_ENTER); - } - ``` - - 2. **添加重启消息**。将此消息添加到你的消息常量中: - - ```javascript - KEY_EVENT_ENTER: "KEY_EVENT_ENTER", - ``` - -5. **实现游戏规则**。实现以下游戏规则: - - 1. **玩家胜利条件**。当所有敌方飞船被摧毁时,显示胜利信息。 - - 1. 首先,创建一个`displayMessage()`函数: - - ```javascript - function displayMessage(message, color = "red") { - ctx.font = "30px Arial"; - ctx.fillStyle = color; - ctx.textAlign = "center"; - ctx.fillText(message, canvas.width / 2, canvas.height / 2); - } - ``` - - 2. 创建一个`endGame()`函数: - - ```javascript - function endGame(win) { - clearInterval(gameLoopId); + }, 200) +} +``` + +**此函数做了什么:** +- **冻结** 所有移动——不再有舰船或激光动作 +- **暂停** 200毫秒,让最后一帧完成绘制 +- **清屏** 并将背景涂成黑色,效果更戏剧化 +- **显示** 不同消息给胜利者和失败者 +- **用色彩区分消息**——绿代表好消息,红代表……不太好 +- **告诉** 玩家如何重新开始游戏 + +### 🔄 **教学反馈点** +**游戏状态管理**:在实现重置功能前确保理解: +- ✅ 结束条件如何创造清晰的游戏目标 +- ✅ 为什么视觉反馈对玩家理解至关重要 +- ✅ 合适清理对防止内存泄漏的重要性 +- ✅ 事件驱动架构如何实现干净的状态切换 + +**自测速答**:如果重置时不清理事件监听器会怎样? +*答案:导致内存泄漏和重复事件处理器,引发不可预测的行为* + +**游戏设计原则**:你正在实现的是: +- **清晰目标**:玩家明确什么是胜利、什么是失败 +- **即时反馈**:游戏状态变化立即被传达 +- **用户控制**:玩家准备好了就能重启 +- **系统可靠性**:适当清理避免bug和性能损耗 + +### 步骤6:实现游戏重置功能 + +重置系统需彻底清理当前游戏状态,初始化清新的游戏会话。保证玩家得到干净的开局,无前一局遗留数据。 + +**创建 `resetGame()` 函数:** + +```javascript +function resetGame() { + if (gameLoopId) { + clearInterval(gameLoopId); + eventEmitter.clear(); + initGame(); + gameLoopId = setInterval(() => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawPoints(); + drawLife(); + updateGameObjects(); + drawGameObjects(ctx); + }, 100); + } +} +``` + +**逐部分理解:** +- **检查** 当前是否有游戏循环正在运行 +- **清除** 现有游戏循环以停止所有当前游戏活动 +- **移除** 所有事件监听器防止内存泄漏 +- **重新初始化** 游戏状态,生成全新对象和变量 +- **启动** 新的游戏循环,包含必需的游戏逻辑 +- **保持** 100毫秒间隔,确保游戏性能一致 + +**将回车键事件处理器添加到 `initGame()` 函数:** + +```javascript +eventEmitter.on(Messages.KEY_EVENT_ENTER, () => { + resetGame(); +}); +``` + +**为你的 EventEmitter 类添加 `clear()` 方法:** + +```javascript +clear() { + this.listeners = {}; +} +``` + +**关键点总结:** +- **将** 回车键按下连接到重置游戏功能 +- **在** 游戏初始化时注册此事件监听器 +- **提供** 一种清理所有事件监听器的干净方式,用于重置 +- **通过** 清空事件处理器避免内存泄漏 +- **将** 监听器对象重置为空以便重新初始化 + +## 恭喜!🎉 + +👽 💥 🚀 你成功从零构建了完整的游戏。像1970年代首批电子游戏程序员一样,你把代码变成了带有合适游戏机制和用户反馈的互动体验。🚀 💥 👽 + +**你完成了:** +- **实现** 完整的胜负条件并向用户反馈 +- **创建** 无缝重启系统,支持连续游戏 +- **设计** 明确的游戏状态视觉传达 +- **管理** 复杂的游戏状态切换和清理 +- **整合** 所有组件成一个连贯、可玩的游戏 + +### 🔄 **教学反馈点** +**完整游戏开发系统**:庆祝你掌握了完整开发流程: +- ✅ 结束条件如何创造满足感玩家体验? +- ✅ 为什么正确的状态管理对游戏稳定性关键? +- ✅ 视觉反馈如何增强玩家理解? +- ✅ 重启系统在玩家留存中扮演什么角色? + +**系统掌控力**:你的完整游戏展现了: +- **全栈游戏开发**:涵盖图形、输入和状态管理 +- **专业架构**:事件驱动系统加上适当清理 +- **用户体验设计**:清晰反馈与直观控制 +- **性能优化**:高效渲染和内存管理 +- **打磨完善**:所有细节让游戏感觉完美 + +**行业级技能**:你实现了: +- **游戏循环架构**:实时系统性能稳定 +- **事件驱动编程**:解耦系统便于扩展 +- **状态管理**:复杂数据处理与生命周期管控 +- **用户界面设计**:清晰沟通与响应式操作 +- **测试与调试**:迭代开发和问题解决 + +### ⚡ **下5分钟你可以做什么** +- [ ] 玩你的完整游戏,测试所有胜负条件 +- [ ] 尝试修改不同结束条件参数 +- [ ] 加入 console.log 语句追踪游戏状态变化 +- [ ] 与朋友分享游戏并收集反馈 + +### 🎯 **这小时你可以完成什么** +- [ ] 完成课后测验,反思你的开发历程 +- [ ] 为胜负状态添加音效效果 +- [ ] 实现额外的结束条件,如限时或奖励目标 +- [ ] 制作不同难度,调整敌人数量 +- [ ] 美化视觉效果,使用更好字体和颜色 + +### 📅 **你的周度游戏开发精通计划** +- [ ] 完成增强版太空游戏,多关卡和进度系统 +- [ ] 增加高级功能,如增强道具、多样敌人类型和特殊武器 +- [ ] 创建保存的高分榜系统 +- [ ] 设计菜单、设置和选项的用户界面 +- [ ] 优化性能,适配不同设备和浏览器 +- [ ] 在线部署你的游戏,与社区分享 +### 🌟 **您的一个月游戏开发职业规划** +- [ ] 制作多个完整游戏,探索不同的类型和机制 +- [ ] 学习高级游戏开发框架,如 Phaser 或 Three.js +- [ ] 参与开源游戏开发项目的贡献 +- [ ] 研究游戏设计原则和玩家心理 +- [ ] 创建展示您游戏开发技能的作品集 +- [ ] 与游戏开发社区连接,持续学习 + +## 🎯 您的完整游戏开发精通时间表 + +```mermaid +timeline + title 完整游戏开发学习进度 + + section 基础(课程1-2) + 游戏架构:项目结构 + :资源管理 + :画布基础 + :事件系统 - // set a delay so we are sure any paints have finished - setTimeout(() => { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = "black"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - if (win) { - displayMessage( - "Victory!!! Pew Pew... - Press [Enter] to start a new game Captain Pew Pew", - "green" - ); - } else { - displayMessage( - "You died !!! Press [Enter] to start a new game Captain Pew Pew" - ); - } - }, 200) - } - ``` - - 2. **重启逻辑**。当所有生命耗尽或玩家赢得游戏时,显示游戏可以重启。此外,当按下*重启*键时重启游戏(你可以决定哪个键映射到重启)。 - - 1. 创建`resetGame()`函数: - - ```javascript - function resetGame() { - if (gameLoopId) { - clearInterval(gameLoopId); - eventEmitter.clear(); - initGame(); - gameLoopId = setInterval(() => { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = "black"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - drawPoints(); - drawLife(); - updateGameObjects(); - drawGameObjects(ctx); - }, 100); - } - } - ``` - - 2. 在`initGame()`中添加调用`eventEmitter`以重置游戏: - - ```javascript - eventEmitter.on(Messages.KEY_EVENT_ENTER, () => { - resetGame(); - }); - ``` - - 3. 为EventEmitter添加一个`clear()`函数: - - ```javascript - clear() { - this.listeners = {}; - } - ``` - -👽 💥 🚀 恭喜你,船长!你的游戏完成了!干得好!🚀 💥 👽 + section 交互系统(课程3-4) + 玩家控制:输入处理 + :移动机制 + :碰撞检测 + :物理模拟 + + section 游戏机制(课程5) + 反馈系统:得分机制 + :生命管理 + :视觉传达 + :玩家激励 + + section 游戏完成(课程6) + 精细化与流程:结束条件 + :状态管理 + :重启系统 + :用户体验 + + section 高级功能(一周) + 增强技能:音频整合 + :视觉特效 + :关卡进度 + :性能优化 + + section 职业发展(一月) + 行业准备:框架精通 + :团队协作 + :作品集开发 + :社区参与 + + section 职业晋升(三月) + 专业化:高级游戏引擎 + :平台部署 + :盈利策略 + :行业网络 +``` +### 🛠️ 您的完整游戏开发工具包总结 ---- +完成整个太空游戏系列后,您现已掌握: +- **游戏架构**:事件驱动系统、游戏循环和状态管理 +- **图形编程**:Canvas API,精灵渲染和视觉效果 +- **输入系统**:键盘处理、碰撞检测和响应式控制 +- **游戏设计**:玩家反馈、进度系统和参与机制 +- **性能优化**:高效渲染、内存管理和帧率控制 +- **用户体验**:清晰沟通、直观控制和细节打磨 +- **专业模式**:整洁代码、调试技术和项目组织 + +**现实应用**:您的游戏开发技能直接适用于: +- **交互式网络应用**:动态界面和实时系统 +- **数据可视化**:动画图表和交互图形 +- **教育技术**:游戏化和吸引人的学习体验 +- **移动开发**:基于触摸的交互和性能优化 +- **模拟软件**:物理引擎和实时建模 +- **创意产业**:互动艺术、娱乐和数字体验 + +**获得的专业技能**:您现在可以: +- **架构**复杂的交互系统从零开始 +- **调试**使用系统化方法的实时应用 +- **优化**性能以流畅用户体验 +- **设计**吸引人的用户界面和交互模式 +- **高效协作**处理技术项目并合理组织代码 + +**掌握的游戏开发概念**: +- **实时系统**:游戏循环、帧率管理和性能 +- **事件驱动架构**:解耦系统和消息传递 +- **状态管理**:复杂数据处理和生命周期管理 +- **用户界面编程**:Canvas 图形和响应式设计 +- **游戏设计理论**:玩家心理和参与机制 + +**下一阶段**:您已准备好探索高级游戏框架、3D 图形、多玩家系统,或转向专业游戏开发职位! + +🌟 **成就解锁**:您已完成完整的游戏开发旅程,从零打造出专业品质的互动体验! + +**欢迎加入游戏开发社区!** 🎮✨ + +## GitHub Copilot Agent 挑战 🚀 + +使用 Agent 模式完成以下挑战: + +**描述:** 通过实现一个关卡进度系统,增加难度和奖励功能,增强太空游戏。 + +**提示:** 创建一个多关卡太空游戏系统,每个关卡中敌舰数量增加,速度和生命值也提高。增加随关卡增长的得分倍增器,并实现当敌人被摧毁时随机出现的强化道具(如速射或护盾)。包括关卡完成奖励,并在屏幕上显示当前关卡与现有的得分和生命值。 + +了解更多关于[agent mode](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode)的信息。 + +## 🚀 可选增强挑战 + +**给您的游戏添加音效**:通过实现音效来增强游戏体验!考虑添加以下音效: + +- **激光射击**,玩家开火时 +- **敌舰摧毁**,敌舰被击中时 +- **英雄受伤**,玩家受击时 +- **胜利音乐**,游戏胜利时 +- **失败音效**,游戏失败时 + +**音频实现示例:** + +```javascript +// 创建音频对象 +const laserSound = new Audio('assets/laser.wav'); +const explosionSound = new Audio('assets/explosion.wav'); + +// 在游戏事件中播放声音 +function playLaserSound() { + laserSound.currentTime = 0; // 重置到开始处 + laserSound.play(); +} +``` -## 🚀 挑战 +**您需要了解:** +- **创建**用于不同音效的 Audio 对象 +- **重置**`currentTime`以支持快速连续音效播放 +- **处理**浏览器自动播放策略,通过用户交互触发声音 +- **管理**音量和时序以提升游戏体验 -添加一个音效!你能为游戏添加音效以增强游戏体验吗?比如在激光命中、英雄飞船死亡或胜利时播放音效?查看这个[sandbox](https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_audio_play)来学习如何使用JavaScript播放音效。 +> 💡 **学习资源**:探索这个[audio sandbox](https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_audio_play),了解更多 JavaScript 游戏中音频实现。 ## 课后测验 @@ -224,13 +670,15 @@ npm start ## 复习与自学 -你的任务是创建一个全新的样本游戏,因此探索一些有趣的游戏,看看你可能会构建哪种类型的游戏。 +您的作业是创建一个新的示例游戏,您可以探索一些有趣的游戏,看看想制作哪种类型的游戏。 -## 作业 +## 任务 -[构建一个样本游戏](assignment.md) +[构建示例游戏](assignment.md) --- + **免责声明**: -本文档使用AI翻译服务[Co-op Translator](https://github.com/Azure/co-op-translator)进行翻译。尽管我们努力确保准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。因使用本翻译而引起的任何误解或误读,我们概不负责。 \ No newline at end of file +本文件由 AI 翻译服务 [Co-op Translator](https://github.com/Azure/co-op-translator) 翻译而成。尽管我们努力确保准确性,但请注意自动翻译可能包含错误或不准确之处。原始文本的原文版本应视为权威来源。对于重要信息,建议采用专业人工翻译。对于因使用本翻译而产生的任何误解或曲解,我们概不负责。 + \ No newline at end of file diff --git a/translations/zh/6-space-game/6-end-condition/assignment.md b/translations/zh/6-space-game/6-end-condition/assignment.md index 33c88e469..bad478f5f 100644 --- a/translations/zh/6-space-game/6-end-condition/assignment.md +++ b/translations/zh/6-space-game/6-end-condition/assignment.md @@ -1,31 +1,173 @@ -# 创建一个示例游戏 +# 构建一个示例游戏 -## 说明 +## 作业概述 -尝试制作一个小型游戏,练习不同的结束条件。可以设置不同的条件,比如达到一定的分数、英雄失去所有生命值或所有怪物被击败。制作一个简单的游戏,比如基于控制台的冒险游戏。以下游戏流程可以作为灵感: +现在你已经掌握了太空游戏中的游戏结束条件和重新开始功能,是时候将这些概念应用到一个全新的游戏体验中。你将设计并构建自己的游戏,展示不同的结束条件模式和重新开始机制。 + +本次作业挑战你在游戏设计上的创造性思维,同时练习所学的技术技能。你将探索不同的胜利和失败场景,实施玩家进度,并创造有趣的重新开始体验。 + +## 项目要求 + +### 核心游戏功能 + +你的游戏必须包含以下基本元素: + +**结束条件多样性**:实现至少两种不同的游戏结束方式: +- **基于积分的胜利**:玩家达到目标分数或收集特定物品 +- **基于生命的失败**:玩家失去所有可用生命或生命值 +- **目标完成**:所有敌人被击败、特定物品被收集或目标达成 +- **基于时间**:游戏在设定时间结束或倒计时归零 + +**重新开始功能**: +- **清除游戏状态**:移除所有之前的游戏对象并重置变量 +- **重新初始化系统**:以新的玩家状态、敌人和目标重新开始 +- **用户友好控制**:提供清晰的重新开始游戏指令 + +**玩家反馈**: +- **胜利信息**:用积极的反馈庆祝玩家成就 +- **失败信息**:提供鼓励性消息激励重玩 +- **进度指示**:显示当前分数、生命或目标状态 + +### 游戏创意与灵感 + +选择以下游戏概念之一或自创: + +#### 1. 控制台冒险游戏 +创建带有战斗机制的文本冒险: ``` Hero> Strikes with broadsword - orc takes 3p damage -Orc> Hits with club - hero takes 2p damage +Orc> Hits with club - hero takes 2p damage Hero> Kicks - orc takes 1p damage Game> Orc is defeated - Hero collects 2 coins Game> ****No more monsters, you have conquered the evil fortress**** ``` -## 评分标准 +**关键功能实现:** +- **回合制战斗**,具有不同攻击选项 +- 玩家和敌人的**生命值** +- 用于收集金币或物品的**物品栏系统** +- 多种难度不同的**敌人类型** +- 当所有敌人被击败时**胜利条件** + +#### 2. 收集游戏 +- **目标**:收集特定物品同时避开障碍 +- **结束条件**:达到目标收集数或失去所有生命 +- **进度**:随着游戏进行,物品变得更难获取 + +#### 3. 拼图游戏 +- **目标**:解决难度不断增加的谜题 +- **结束条件**:完成所有关卡或用尽移动/时间 +- **重新开始**:重置为第一关,清除进度 + +#### 4. 防御游戏 +- **目标**:保护基地免受一波波敌人攻击 +- **结束条件**:存活所有波次(胜利)或基地被摧毁(失败) +- **进度**:敌波难度和数量增加 + +## 实施指南 + +### 入门 + +1. **规划你的游戏设计**: + - 绘制基本的游戏循环 + - 明确你的结束条件 + - 确定重新开始时需要重置的数据 + +2. **设置项目结构**: + ``` + my-game/ + ├── index.html + ├── style.css + ├── game.js + └── README.md + ``` + +3. **创建核心游戏循环**: + - 初始化游戏状态 + - 处理用户输入 + - 更新游戏逻辑 + - 检查结束条件 + - 渲染当前状态 + +### 技术要求 + +**使用现代JavaScript**: +- 使用`const`和`let`声明变量 +- 适当使用箭头函数 +- 实现ES6+功能,如模板字符串和解构赋值 + +**事件驱动架构**: +- 创建用户交互事件处理器 +- 通过事件实现游戏状态变化 +- 使用事件监听实现重新开始功能 + +**清洁代码实践**: +- 编写单一职责函数 +- 使用描述性变量和函数名 +- 添加注释说明游戏逻辑和规则 +- 将代码组织成逻辑部分 + +## 提交要求 + +### 交付内容 + +1. **完整游戏文件**:运行游戏所需的所有HTML、CSS和JavaScript文件 +2. **README.md**:文档说明: + - 如何玩你的游戏 + - 你实现了哪些结束条件 + - 重新开始的操作说明 + - 任何特别的功能或机制 +3. **代码注释**:清晰解释游戏逻辑和算法 + +### 测试清单 + +提交前,请确认你的游戏: + +- [ ] **在浏览器控制台无错误运行** +- [ ] **实现多种结束条件** +- [ ] **重新开始时正确重置状态** +- [ ] **向玩家提供清晰的游戏状态反馈** +- [ ] **使用现代JavaScript语法和最佳实践** +- [ ] **README.md中包含完整文档** + +## 评估标准 + +| 评分标准 | 优秀 (4) | 良好 (3) | 中等 (2) | 初级 (1) | +|----------|----------|----------|----------|----------| +| **游戏功能** | 完整的游戏,多个结束条件,流畅重启,玩法体验优秀 | 完整游戏,基本结束条件,功能性重启机制 | 部分游戏,部分结束条件实现,重启存在小问题 | 不完整的游戏,功能受限且存在显著缺陷 | +| **代码质量** | 代码整洁,组织良好,使用现代JavaScript,注释详尽,结构优秀 | 代码组织良好,语法现代,注释充分,结构清晰 | 基础代码组织,部分现代语法,注释少 | 代码组织差,语法过时,缺少注释和结构 | +| **用户体验** | 游戏直观,指令清晰,反馈优秀,结束和重启体验吸引人 | 性能良好,有基本指令和反馈,结束和重启功能正常 | 基础玩法,指令少,游戏状态反馈有限 | 游戏混乱,指令不清,反馈差 | +| **技术实现** | 精通游戏开发概念,事件处理和状态管理 | 理解游戏概念,实施良好 | 基础理解,能实现但不完善 | 理解有限,实现较差 | +| **文档** | README详尽清晰,代码注释充分,测试证明完整 | 良好文档,指令清晰,注释足够 | 基础文档,指令少 | 缺少或无文档 | + +### 评分等级 +- **优秀 (16-20分)**:超出预期,创意丰富,实施完善 +- **良好 (12-15分)**:满足所有要求,执行稳健 +- **中等 (8-11分)**:大部分要求满足,有些小问题 +- **初级 (4-7分)**:部分要求满足,需大幅改进 + +## 额外学习资源 + +- [MDN 游戏开发指南](https://developer.mozilla.org/en-US/docs/Games) +- [JavaScript 游戏开发教程](https://developer.mozilla.org/en-US/docs/Games/Tutorials) +- [Canvas API 文档](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) +- [游戏设计原则](https://www.gamasutra.com/blogs/) + +> 💡 **专业提示**:从简单开始,逐步添加功能。一个打磨精良的简单游戏胜过一个充满漏洞的复杂游戏! -| 标准 | 卓越表现 | 合格表现 | 需要改进 | -| -------- | ---------------------- | -------------------------- | -------------------------- | -| | 完整的游戏呈现 | 部分游戏呈现 | 部分游戏存在漏洞 | +--- -**免责声明**: -本文档使用AI翻译服务[Co-op Translator](https://github.com/Azure/co-op-translator)进行翻译。尽管我们努力确保翻译的准确性,但请注意,自动翻译可能包含错误或不准确之处。原始语言的文档应被视为权威来源。对于关键信息,建议使用专业人工翻译。我们不对因使用此翻译而产生的任何误解或误读承担责任。 \ No newline at end of file + +**免责声明**: +本文档使用AI翻译服务[Co-op Translator](https://github.com/Azure/co-op-translator)进行翻译。尽管我们力求准确,但请注意自动翻译可能包含错误或不准确之处。原始语言的文档应被视为权威来源。对于重要信息,建议采用专业人工翻译。我们不对因使用本翻译而产生的任何误解或曲解负责。 + \ No newline at end of file diff --git a/translations/zh/7-bank-project/1-template-route/README.md b/translations/zh/7-bank-project/1-template-route/README.md index 8237d9ef1..e98c0b514 100644 --- a/translations/zh/7-bank-project/1-template-route/README.md +++ b/translations/zh/7-bank-project/1-template-route/README.md @@ -1,31 +1,96 @@ -# 构建银行应用程序第1部分:Web应用中的HTML模板和路由 - +# 构建银行应用第1部分:Web应用中的HTML模板与路由 + +```mermaid +journey + title 你的银行应用开发旅程 + section SPA 基础 + 了解单页应用: 3: Student + 学习模板概念: 4: Student + 精通 DOM 操作: 4: Student + section 路由系统 + 实现客户端路由: 4: Student + 处理浏览器历史: 5: Student + 创建导航系统: 5: Student + section 专业模式 + 构建模块化架构: 5: Student + 应用最佳实践: 5: Student + 创建用户体验: 5: Student +``` +1969年,当阿波罗11号的导航计算机飞向月球时,它必须在不重启整个系统的情况下切换不同程序。现代Web应用类似——它们改变你看到的内容而不重新加载所有内容。这创造了用户如今期望的流畅、响应式体验。 + +与每次交互都重新加载整个页面的传统网站不同,现代Web应用只更新需要更改的部分。这种方法就像任务控制中心在保持持续通信的同时切换不同显示屏一样,产生了我们习以为常的流畅体验。 + +以下是造成差异的关键: + +| 传统多页面应用 | 现代单页面应用 | +|----------------------------|-------------------------| +| **导航** | 每个屏幕都全页重新加载 | 内容瞬时切换 | +| **性能** | 由于完整HTML下载较慢 | 通过局部更新更快 | +| **用户体验** | 令人不适的页面闪烁 | 平滑的类应用过渡 | +| **数据共享** | 页面间共享困难 | 状态管理简单 | +| **开发** | 维护多个HTML文件 | 单一HTML与动态模板 | + +**理解演变:** +- **传统应用** 每次导航都需服务器请求 +- **现代SPA** 一次加载,使用JavaScript动态更新内容 +- **用户期望** 现偏好即时、无缝交互 +- **性能优势** 包括带宽减少和响应加快 + +本课将构建一个拥有多个无缝流转页面的银行应用。就像科学家使用可重构的模块化仪器进行不同实验一样,我们将使用HTML模板作为可重复使用的组件,根据需要显示。 + +你将使用HTML模板(不同画面的可重用蓝图)、JavaScript路由(切换不同屏幕的系统)和浏览器历史API(保持返回按钮正常工作)。这些是React、Vue和Angular等框架的基本技术。 + +最终,你将获得一个展示专业单页面应用原则的可用银行应用。 + +```mermaid +mindmap + root((单页应用程序)) + Architecture + 模板系统 + 客户端路由 + 状态管理 + 事件处理 + Templates + 可复用组件 + 动态内容 + DOM操作 + 内容切换 + Routing + URL管理 + 历史API + 导航逻辑 + 浏览器集成 + User Experience + 快速导航 + 平滑过渡 + 状态一致 + 现代交互 + Performance + 减少服务器请求 + 更快的页面切换 + 高效资源使用 + 更佳响应性 +``` ## 课前测验 [课前测验](https://ff-quizzes.netlify.app/web/quiz/41) -### 简介 - -自从浏览器中出现JavaScript以来,网站变得比以往更加互动和复杂。如今,Web技术常被用来创建直接在浏览器中运行的功能齐全的应用程序,我们称之为[Web应用程序](https://en.wikipedia.org/wiki/Web_application)。由于Web应用程序高度互动,用户不希望每次执行操作时都需要等待整个页面重新加载。因此,JavaScript被用来直接通过DOM更新HTML,以提供更流畅的用户体验。 +### 你需要准备的内容 -在本课程中,我们将为创建银行Web应用程序奠定基础,使用HTML模板创建多个屏幕,这些屏幕可以显示和更新,而无需重新加载整个HTML页面。 - -### 前置条件 - -你需要一个本地Web服务器来测试我们将在本课程中构建的Web应用程序。如果你没有,可以安装[Node.js](https://nodejs.org),然后在项目文件夹中使用命令`npx lite-server`。它会创建一个本地Web服务器并在浏览器中打开你的应用程序。 +我们需要一个本地Web服务器来测试银行应用——别担心,没你想象的难!如果你还没有配置,只需安装 [Node.js](https://nodejs.org) 并在项目文件夹运行 `npx lite-server`。这个命令会启动本地服务器并自动在浏览器打开应用。 ### 准备工作 -在你的电脑上创建一个名为`bank`的文件夹,并在其中创建一个名为`index.html`的文件。我们将从这个HTML[样板代码](https://en.wikipedia.org/wiki/Boilerplate_code)开始: +在你的电脑上创建一个名为 `bank` 的文件夹,里面放一个名为 `index.html` 的文件。我们将从这个HTML [模板](https://en.wikipedia.org/wiki/Boilerplate_code)开始: ```html @@ -41,30 +106,77 @@ CO_OP_TRANSLATOR_METADATA: ``` +**这个模板提供了:** +- **建立** 正确DOCTYPE声明的HTML5文档结构 +- **设置** 字符编码为UTF-8以支持国际文本 +- **启用** 响应式设计,使用viewport元标签兼容移动设备 +- **设定** 浏览器标签页显示的描述性标题 +- **创建** 一个干净的主体部分,用于构建应用 + +> 📁 **项目结构预览** +> +> **完成此课时后,项目将包含:** +> ``` +> bank/ +> ├── index.html +> ├── app.js +> └── style.css +> ``` +> +> **文件职责:** +> - **index.html**:包含所有模板和应用结构 +> - **app.js**:负责路由、导航和模板管理 +> - **模板**:定义登录、仪表盘及其他屏幕的UI + --- ## HTML模板 -如果你想为一个网页创建多个屏幕,一种解决方案是为每个屏幕创建一个HTML文件。然而,这种解决方案有一些不便之处: +模板解决了Web开发中的一个根本问题。古腾堡在1440年代发明活字印刷时意识到,与其刻画整页,他可以制作可复用的字母块并按需排列。HTML模板原理相同——不为每个屏幕创建单独HTML文件,而是定义可复用结构,按需显示。 + +```mermaid +flowchart TD + A["📋 模板定义"] --> B["💬 在DOM中隐藏"] + B --> C["🔍 JavaScript查找模板"] + C --> D["📋 克隆模板内容"] + D --> E["🔗 附加到可见DOM"] + E --> F["👁️ 用户看到内容"] + + G["登录模板"] --> A + H["仪表板模板"] --> A + I["未来模板"] --> A + + style A fill:#e3f2fd + style D fill:#e8f5e8 + style F fill:#fff3e0 + style B fill:#f3e5f5 +``` +把模板看作应用不同部分的蓝图。就像建筑师绘制一个蓝图多次使用,而不是重复绘制相同房间,我们创建模板一次,按需实例化。浏览器会将这些模板隐藏,直到JavaScript激活它们。 -- 切换屏幕时需要重新加载整个HTML,这可能会很慢。 -- 在不同屏幕之间共享数据会变得困难。 +如果想为网页创建多个屏幕,一种办法是每个屏幕一个HTML文件。然而,这样做有不便: -另一种方法是只使用一个HTML文件,并使用`