CINXE.COM
剖析游戏结构 - 游戏开发 | MDN
<!doctype html><html lang="en-US" prefix="og: https://ogp.me/ns#"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="icon" href="https://developer.mozilla.org/favicon-48x48.bc390275e955dacb2e65.png"/><link rel="apple-touch-icon" href="https://developer.mozilla.org/apple-touch-icon.528534bba673c38049c2.png"/><meta name="theme-color" content="#ffffff"/><link rel="manifest" href="https://developer.mozilla.org/manifest.f42880861b394dd4dc9b.json"/><link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="MDN Web Docs"/><title>剖析游戏结构 - 游戏开发 | MDN</title><link rel="alternate" title="Anatomie eines Videospiels" href="https://developer.mozilla.org/de/docs/Games/Anatomy" hrefLang="de"/><link rel="alternate" title="Anatomy of a video game" href="https://developer.mozilla.org/en-US/docs/Games/Anatomy" hrefLang="en"/><link rel="alternate" title="Anatomía de un videojuego" href="https://developer.mozilla.org/es/docs/Games/Anatomy" hrefLang="es"/><link rel="alternate" title="Anatomie d'un jeu vidéo" href="https://developer.mozilla.org/fr/docs/Games/Anatomy" hrefLang="fr"/><link rel="alternate" title="ビデオゲームの解剖学" href="https://developer.mozilla.org/ja/docs/Games/Anatomy" hrefLang="ja"/><link rel="alternate" title="Anatomia de um vídeo game" href="https://developer.mozilla.org/pt-BR/docs/Games/Anatomy" hrefLang="pt"/><link rel="alternate" title="Анатомия видеоигры" href="https://developer.mozilla.org/ru/docs/Games/Anatomy" hrefLang="ru"/><link rel="alternate" title="剖析游戏结构" href="https://developer.mozilla.org/zh-CN/docs/Games/Anatomy" hrefLang="zh"/><link rel="preload" as="font" type="font/woff2" href="/static/media/Inter.var.c2fe3cb2b7c746f7966a.woff2" crossorigin=""/><link rel="alternate" type="application/rss+xml" title="MDN Blog RSS Feed" href="https://developer.mozilla.org/en-US/blog/rss.xml" hrefLang="en"/><meta name="description" content="本文从技术角度分析了一般电子游戏的结构和工作流程,就此介绍主循环是如何运行的。它有助于初学者了解在现代游戏开发领域构建游戏时需要什么,以及理解像 JavaScript 这样的 Web 标准是如何成为可用于开发游戏的工具的。游戏开发经验丰富但不熟悉 Web 开发的开发者也能从本文中受益。"/><meta property="og:url" content="https://developer.mozilla.org/zh-CN/docs/Games/Anatomy"/><meta property="og:title" content="剖析游戏结构 - 游戏开发 | MDN"/><meta property="og:type" content="website"/><meta property="og:locale" content="zh_CN"/><meta property="og:description" content="本文从技术角度分析了一般电子游戏的结构和工作流程,就此介绍主循环是如何运行的。它有助于初学者了解在现代游戏开发领域构建游戏时需要什么,以及理解像 JavaScript 这样的 Web 标准是如何成为可用于开发游戏的工具的。游戏开发经验丰富但不熟悉 Web 开发的开发者也能从本文中受益。"/><meta property="og:image" content="https://developer.mozilla.org/mdn-social-share.d893525a4fb5fb1f67a2.png"/><meta property="og:image:type" content="image/png"/><meta property="og:image:height" content="1080"/><meta property="og:image:width" content="1920"/><meta property="og:image:alt" content="The MDN Web Docs logo, featuring a blue accent color, displayed on a solid black background."/><meta property="og:site_name" content="MDN Web Docs"/><meta name="twitter:card" content="summary_large_image"/><meta name="twitter:creator" content="MozDevNet"/><link rel="canonical" href="https://developer.mozilla.org/zh-CN/docs/Games/Anatomy"/><style media="print">.article-actions-container,.document-toc-container,.language-menu,.main-menu-toggle,.on-github,.page-footer,.place,.sidebar,.top-banner,.top-navigation-main,ul.prev-next{display:none!important}.main-page-content,.main-page-content pre{padding:2px}.main-page-content pre{border-left-width:2px}</style><script src="/static/js/gtag.js" defer=""></script><script defer="" src="/static/js/main.1b60bff1.js"></script><link href="/static/css/main.959b5ea9.css" rel="stylesheet"/></head><body><script>if(document.body.addEventListener("load",(t=>{t.target.classList.contains("interactive")&&t.target.setAttribute("data-readystate","complete")}),{capture:!0}),window&&document.documentElement){const t={light:"#ffffff",dark:"#1b1b1b"};try{const e=window.localStorage.getItem("theme");e&&(document.documentElement.className=e,document.documentElement.style.backgroundColor=t[e]);const o=window.localStorage.getItem("nop");o&&(document.documentElement.dataset.nop=o)}catch(t){console.warn("Unable to read theme from localStorage",t)}}</script><div id="root"><ul id="nav-access" class="a11y-nav"><li><a id="skip-main" href="#content">Skip to main content</a></li><li><a id="skip-search" href="#top-nav-search-input">Skip to search</a></li><li><a id="skip-select-language" href="#languages-switcher-button">Skip to select language</a></li></ul><div class="page-wrapper document-page"><div class="top-banner loading"><section class="place top container"></section></div><div class="sticky-header-container"><header class="top-navigation "><div class="container "><div class="top-navigation-wrap"><a href="/zh-CN/" class="logo" aria-label="MDN homepage"><svg id="mdn-docs-logo" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 694.9 104.4" style="enable-background:new 0 0 694.9 104.4" xml:space="preserve" role="img"><title>MDN Web Docs</title><path d="M40.3 0 11.7 92.1H0L28.5 0h11.8zm10.4 0v92.1H40.3V0h10.4zM91 0 62.5 92.1H50.8L79.3 0H91zm10.4 0v92.1H91V0h10.4z" class="logo-m"></path><path d="M627.9 95.6h67v8.8h-67v-8.8z" class="logo-_"></path><path d="M367 42h-4l-10.7 30.8h-5.5l-10.8-26h-.4l-10.5 26h-5.2L308.7 42h-3.8v-5.6H323V42h-6.5l6.8 20.4h.4l10.3-26h4.7l11.2 26h.5l5.7-20.3h-6.2v-5.6H367V42zm34.9 20c-.4 3.2-2 5.9-4.7 8.2-2.8 2.3-6.5 3.4-11.3 3.4-5.4 0-9.7-1.6-13.1-4.7-3.3-3.2-5-7.7-5-13.7 0-5.7 1.6-10.3 4.7-14s7.4-5.5 12.9-5.5c5.1 0 9.1 1.6 11.9 4.7s4.3 6.9 4.3 11.3c0 1.5-.2 3-.5 4.7h-25.6c.3 7.7 4 11.6 10.9 11.6 2.9 0 5.1-.7 6.5-2 1.5-1.4 2.5-3 3-4.9l6 .9zM394 51.3c.2-2.4-.4-4.7-1.8-6.9s-3.8-3.3-7-3.3c-3.1 0-5.3 1-6.9 3-1.5 2-2.5 4.4-2.8 7.2H394zm51 2.4c0 5-1.3 9.5-4 13.7s-6.9 6.2-12.7 6.2c-6 0-10.3-2.2-12.7-6.7-.1.4-.2 1.4-.4 2.9s-.3 2.5-.4 2.9h-7.3c.3-1.7.6-3.5.8-5.3.3-1.8.4-3.7.4-5.5V22.3h-6v-5.6H416v27c1.1-2.2 2.7-4.1 4.7-5.7 2-1.6 4.8-2.4 8.4-2.4 4.6 0 8.4 1.6 11.4 4.7 3 3.2 4.5 7.6 4.5 13.4zm-7.7.6c0-4.2-1-7.4-3-9.5-2-2.2-4.4-3.3-7.4-3.3-3.4 0-6 1.2-8 3.7-1.9 2.4-2.9 5-3 7.7V57c0 3 1 5.6 3 7.7s4.5 3.1 7.6 3.1c3.6 0 6.3-1.3 8.1-3.9 1.8-2.7 2.7-5.9 2.7-9.6zm69.2 18.5h-13.2v-7.2c-1.2 2.2-2.8 4.1-4.9 5.6-2.1 1.6-4.8 2.4-8.3 2.4-4.8 0-8.7-1.6-11.6-4.9-2.9-3.2-4.3-7.7-4.3-13.3 0-5 1.3-9.6 4-13.7 2.6-4.1 6.9-6.2 12.8-6.2 5.7 0 9.8 2.2 12.3 6.5V22.3h-8.6v-5.6h15.8v50.6h6v5.5zM493.2 56v-4.4c-.1-3-1.2-5.5-3.2-7.3s-4.4-2.8-7.2-2.8c-3.6 0-6.3 1.3-8.2 3.9-1.9 2.6-2.8 5.8-2.8 9.6 0 4.1 1 7.3 3 9.5s4.5 3.3 7.4 3.3c3.2 0 5.8-1.3 7.8-3.8 2.1-2.6 3.1-5.3 3.2-8zm53.1-1.4c0 5.6-1.8 10.2-5.3 13.7s-8.2 5.3-13.9 5.3-10.1-1.7-13.4-5.1c-3.3-3.4-5-7.9-5-13.5 0-5.3 1.6-9.9 4.7-13.7 3.2-3.8 7.9-5.7 14.2-5.7s11 1.9 14.1 5.7c3 3.7 4.6 8.1 4.6 13.3zm-7.7-.2c0-4-1-7.2-3-9.5s-4.8-3.5-8.2-3.5c-3.6 0-6.4 1.2-8.3 3.7s-2.9 5.6-2.9 9.5c0 3.7.9 6.8 2.8 9.4 1.9 2.6 4.6 3.9 8.3 3.9 3.6 0 6.4-1.3 8.4-3.8 1.9-2.6 2.9-5.8 2.9-9.7zm45 5.8c-.4 3.2-1.9 6.3-4.4 9.1-2.5 2.9-6.4 4.3-11.8 4.3-5.2 0-9.4-1.6-12.6-4.8-3.2-3.2-4.8-7.7-4.8-13.7 0-5.5 1.6-10.1 4.7-13.9 3.2-3.8 7.6-5.7 13.2-5.7 2.3 0 4.6.3 6.7.8 2.2.5 4.2 1.5 6.2 2.9l1.5 9.5-5.9.7-1.3-6.1c-2.1-1.2-4.5-1.8-7.2-1.8-3.5 0-6.1 1.2-7.7 3.7-1.7 2.5-2.5 5.7-2.5 9.6 0 4.1.9 7.3 2.7 9.5 1.8 2.3 4.4 3.4 7.8 3.4 5.2 0 8.2-2.9 9.2-8.8l6.2 1.3zm34.7 1.9c0 3.6-1.5 6.5-4.6 8.5s-7 3-11.7 3c-5.7 0-10.6-1.2-14.6-3.6l1.2-8.8 5.7.6-.2 4.7c1.1.5 2.3.9 3.6 1.1s2.6.3 3.9.3c2.4 0 4.5-.4 6.5-1.3 1.9-.9 2.9-2.2 2.9-4.1 0-1.8-.8-3.1-2.3-3.8s-3.5-1.3-5.8-1.7-4.6-.9-6.9-1.4c-2.3-.6-4.2-1.6-5.7-2.9-1.6-1.4-2.3-3.5-2.3-6.3 0-4.1 1.5-6.9 4.6-8.5s6.4-2.4 9.9-2.4c2.6 0 5 .3 7.2.9 2.2.6 4.3 1.4 6.1 2.4l.8 8.8-5.8.7-.8-5.7c-2.3-1-4.7-1.6-7.2-1.6-2.1 0-3.7.4-5.1 1.1-1.3.8-2 2-2 3.8 0 1.7.8 2.9 2.3 3.6 1.5.7 3.4 1.2 5.7 1.6 2.2.4 4.5.8 6.7 1.4 2.2.6 4.1 1.6 5.7 3 1.4 1.6 2.2 3.7 2.2 6.6zM197.6 73.2h-17.1v-5.5h3.8V51.9c0-3.7-.7-6.3-2.1-7.9-1.4-1.6-3.3-2.3-5.7-2.3-3.2 0-5.6 1.1-7.2 3.4s-2.4 4.6-2.5 6.9v15.6h6v5.5h-17.1v-5.5h3.8V51.9c0-3.8-.7-6.4-2.1-7.9-1.4-1.5-3.3-2.3-5.6-2.3-3.2 0-5.5 1.1-7.2 3.3-1.6 2.2-2.4 4.5-2.5 6.9v15.8h6.9v5.5h-20.2v-5.5h6V42.4h-6.1v-5.6h13.4v6.4c1.2-2.1 2.7-3.8 4.7-5.2 2-1.3 4.4-2 7.3-2s5.3.7 7.5 2.1c2.2 1.4 3.7 3.5 4.5 6.4 1.1-2.5 2.7-4.5 4.9-6.1s4.8-2.4 7.9-2.4c3.5 0 6.5 1.1 8.9 3.3s3.7 5.6 3.7 10.2v18.2h6.1v5.5zm42.5 0h-13.2V66c-1.2 2.2-2.8 4.1-4.9 5.6-2.1 1.6-4.8 2.4-8.3 2.4-4.8 0-8.7-1.6-11.6-4.9-2.9-3.2-4.3-7.7-4.3-13.3 0-5 1.3-9.6 4-13.7 2.6-4.1 6.9-6.2 12.8-6.2s9.8 2.2 12.3 6.5V22.7h-8.6v-5.6h15.8v50.6h6v5.5zm-13.3-16.8V52c-.1-3-1.2-5.5-3.2-7.3s-4.4-2.8-7.2-2.8c-3.6 0-6.3 1.3-8.2 3.9-1.9 2.6-2.8 5.8-2.8 9.6 0 4.1 1 7.3 3 9.5s4.5 3.3 7.4 3.3c3.2 0 5.8-1.3 7.8-3.8 2.1-2.6 3.1-5.3 3.2-8zm61.5 16.8H269v-5.5h6V51.9c0-3.7-.7-6.3-2.2-7.9-1.4-1.6-3.4-2.3-5.7-2.3-3.1 0-5.6 1-7.4 3s-2.8 4.4-2.9 7v15.9h6v5.5h-19.3v-5.5h6V42.4h-6.2v-5.6h13.6V43c2.6-4.6 6.8-6.9 12.7-6.9 3.6 0 6.7 1.1 9.2 3.3s3.7 5.6 3.7 10.2v18.2h6v5.4h-.2z" class="logo-text"></path></svg></a><button title="Open main menu" type="button" class="button action has-icon main-menu-toggle" aria-haspopup="menu" aria-label="Open main menu" aria-expanded="false"><span class="button-wrap"><span class="icon icon-menu "></span><span class="visually-hidden">Open main menu</span></span></button></div><div class="top-navigation-main"><nav class="main-nav" aria-label="Main menu"><ul class="main-menu nojs"><li class="top-level-entry-container "><button type="button" id="references-button" class="top-level-entry menu-toggle" aria-controls="references-menu" aria-expanded="false">References</button><a href="/zh-CN/docs/Web" class="top-level-entry">References</a><ul id="references-menu" class="submenu references hidden inline-submenu-lg" aria-labelledby="references-button"><li class="apis-link-container mobile-only "><a href="/zh-CN/docs/Web" class="submenu-item "><div class="submenu-icon"></div><div class="submenu-content-container"><div class="submenu-item-heading">Overview / Web Technology</div><p class="submenu-item-description">Web technology reference for developers</p></div></a></li><li class="html-link-container "><a href="/zh-CN/docs/Web/HTML" class="submenu-item "><div class="submenu-icon html"></div><div class="submenu-content-container"><div class="submenu-item-heading">HTML</div><p class="submenu-item-description">Structure of content on the web</p></div></a></li><li class="css-link-container "><a href="/zh-CN/docs/Web/CSS" class="submenu-item "><div class="submenu-icon css"></div><div class="submenu-content-container"><div class="submenu-item-heading">CSS</div><p class="submenu-item-description">Code used to describe document style</p></div></a></li><li class="javascript-link-container "><a href="/zh-CN/docs/Web/JavaScript" class="submenu-item "><div class="submenu-icon javascript"></div><div class="submenu-content-container"><div class="submenu-item-heading">JavaScript</div><p class="submenu-item-description">General-purpose scripting language</p></div></a></li><li class="http-link-container "><a href="/zh-CN/docs/Web/HTTP" class="submenu-item "><div class="submenu-icon http"></div><div class="submenu-content-container"><div class="submenu-item-heading">HTTP</div><p class="submenu-item-description">Protocol for transmitting web resources</p></div></a></li><li class="apis-link-container "><a href="/zh-CN/docs/Web/API" class="submenu-item "><div class="submenu-icon apis"></div><div class="submenu-content-container"><div class="submenu-item-heading">Web APIs</div><p class="submenu-item-description">Interfaces for building web applications</p></div></a></li><li class="apis-link-container "><a href="/zh-CN/docs/Mozilla/Add-ons/WebExtensions" class="submenu-item "><div class="submenu-icon"></div><div class="submenu-content-container"><div class="submenu-item-heading">Web Extensions</div><p class="submenu-item-description">Developing extensions for web browsers</p></div></a></li><li class="apis-link-container desktop-only "><a href="/zh-CN/docs/Web" class="submenu-item "><div class="submenu-icon"></div><div class="submenu-content-container"><div class="submenu-item-heading">Web Technology</div><p class="submenu-item-description">Web technology reference for developers</p></div></a></li></ul></li><li class="top-level-entry-container "><button type="button" id="guides-button" class="top-level-entry menu-toggle" aria-controls="guides-menu" aria-expanded="false">Guides</button><a href="/zh-CN/docs/Learn" class="top-level-entry">Guides</a><ul id="guides-menu" class="submenu guides hidden inline-submenu-lg" aria-labelledby="guides-button"><li class="apis-link-container mobile-only "><a href="/zh-CN/docs/Learn" class="submenu-item "><div class="submenu-icon learn"></div><div class="submenu-content-container"><div class="submenu-item-heading">Overview / MDN Learning Area</div><p class="submenu-item-description">Learn web development</p></div></a></li><li class="apis-link-container desktop-only "><a href="/zh-CN/docs/Learn" class="submenu-item "><div class="submenu-icon learn"></div><div class="submenu-content-container"><div class="submenu-item-heading">MDN Learning Area</div><p class="submenu-item-description">Learn web development</p></div></a></li><li class="html-link-container "><a href="/zh-CN/docs/Learn/HTML" class="submenu-item "><div class="submenu-icon html"></div><div class="submenu-content-container"><div class="submenu-item-heading">HTML</div><p class="submenu-item-description">Learn to structure web content with HTML</p></div></a></li><li class="css-link-container "><a href="/zh-CN/docs/Learn/CSS" class="submenu-item "><div class="submenu-icon css"></div><div class="submenu-content-container"><div class="submenu-item-heading">CSS</div><p class="submenu-item-description">Learn to style content using CSS</p></div></a></li><li class="javascript-link-container "><a href="/zh-CN/docs/Learn/JavaScript" class="submenu-item "><div class="submenu-icon javascript"></div><div class="submenu-content-container"><div class="submenu-item-heading">JavaScript</div><p class="submenu-item-description">Learn to run scripts in the browser</p></div></a></li><li class=" "><a href="/zh-CN/docs/Web/Accessibility" class="submenu-item "><div class="submenu-icon"></div><div class="submenu-content-container"><div class="submenu-item-heading">Accessibility</div><p class="submenu-item-description">Learn to make the web accessible to all</p></div></a></li></ul></li><li class="top-level-entry-container "><button type="button" id="mdn-plus-button" class="top-level-entry menu-toggle" aria-controls="mdn-plus-menu" aria-expanded="false">Plus</button><a href="/zh-CN/plus" class="top-level-entry">Plus</a><ul id="mdn-plus-menu" class="submenu mdn-plus hidden inline-submenu-lg" aria-labelledby="mdn-plus-button"><li class=" "><a href="/zh-CN/plus" class="submenu-item "><div class="submenu-icon"></div><div class="submenu-content-container"><div class="submenu-item-heading">Overview</div><p class="submenu-item-description">A customized MDN experience</p></div></a></li><li class=" "><a href="/zh-CN/plus/ai-help" class="submenu-item "><div class="submenu-icon"></div><div class="submenu-content-container"><div class="submenu-item-heading">AI Help</div><p class="submenu-item-description">Get real-time assistance and support</p></div></a></li><li class=" "><a href="/zh-CN/plus/updates" class="submenu-item "><div class="submenu-icon"></div><div class="submenu-content-container"><div class="submenu-item-heading">Updates</div><p class="submenu-item-description">All browser compatibility updates at a glance</p></div></a></li><li class=" "><a href="/en-US/plus/docs/features/overview" class="submenu-item "><div class="submenu-icon"></div><div class="submenu-content-container"><div class="submenu-item-heading">Documentation</div><p class="submenu-item-description">Learn how to use MDN Plus</p></div></a></li><li class=" "><a href="/en-US/plus/docs/faq" class="submenu-item "><div class="submenu-icon"></div><div class="submenu-content-container"><div class="submenu-item-heading">FAQ</div><p class="submenu-item-description">Frequently asked questions about MDN Plus</p></div></a></li></ul></li><li class="top-level-entry-container "><a class="top-level-entry menu-link" href="/en-US/curriculum/">Curriculum <sup class="new">New</sup></a></li><li class="top-level-entry-container "><a class="top-level-entry menu-link" href="/en-US/blog/">Blog</a></li><li class="top-level-entry-container "><button type="button" id="tools-button" class="top-level-entry menu-toggle" aria-controls="tools-menu" aria-expanded="false">Tools</button><ul id="tools-menu" class="submenu tools hidden inline-submenu-lg" aria-labelledby="tools-button"><li class=" "><a href="/zh-CN/play" class="submenu-item "><div class="submenu-icon"></div><div class="submenu-content-container"><div class="submenu-item-heading">Playground</div><p class="submenu-item-description">Write, test and share your code</p></div></a></li><li class=" "><a href="/en-US/observatory" class="submenu-item "><div class="submenu-icon"></div><div class="submenu-content-container"><div class="submenu-item-heading">HTTP Observatory</div><p class="submenu-item-description">Scan a website for free</p></div></a></li><li class=" "><a href="/en-US/plus/ai-help" class="submenu-item "><div class="submenu-icon"></div><div class="submenu-content-container"><div class="submenu-item-heading">AI Help</div><p class="submenu-item-description">Get real-time assistance and support</p></div></a></li></ul></li></ul></nav><div class="header-search"><form action="/zh-CN/search" class="search-form search-widget" id="top-nav-search-form" role="search"><label id="top-nav-search-label" for="top-nav-search-input" class="visually-hidden">Search MDN</label><input aria-activedescendant="" aria-autocomplete="list" aria-controls="top-nav-search-menu" aria-expanded="false" aria-labelledby="top-nav-search-label" autoComplete="off" id="top-nav-search-input" role="combobox" type="search" class="search-input-field" name="q" placeholder=" " required="" value=""/><button type="button" class="button action has-icon clear-search-button"><span class="button-wrap"><span class="icon icon-cancel "></span><span class="visually-hidden">Clear search input</span></span></button><button type="submit" class="button action has-icon search-button"><span class="button-wrap"><span class="icon icon-search "></span><span class="visually-hidden">Search</span></span></button><div id="top-nav-search-menu" role="listbox" aria-labelledby="top-nav-search-label"></div></form></div><div class="theme-switcher-menu"><button type="button" class="button action has-icon theme-switcher-menu small" aria-haspopup="menu"><span class="button-wrap"><span class="icon icon-theme-os-default "></span>Theme</span></button></div><ul class="auth-container"><li><a href="/users/fxa/login/authenticate/?next=%2Fzh-CN%2Fdocs%2FGames%2FAnatomy" class="login-link" rel="nofollow">Log in</a></li><li><a href="/users/fxa/login/authenticate/?next=%2Fzh-CN%2Fdocs%2FGames%2FAnatomy" target="_self" rel="nofollow" class="button primary mdn-plus-subscribe-link"><span class="button-wrap">Sign up for free</span></a></li></ul></div></div></header><div class="article-actions-container"><div class="container"><button type="button" class="button action has-icon sidebar-button" aria-label="Expand sidebar" aria-expanded="false" aria-controls="sidebar-quicklinks"><span class="button-wrap"><span class="icon icon-sidebar "></span></span></button><nav class="breadcrumbs-container" aria-label="Breadcrumb"><ol typeof="BreadcrumbList" vocab="https://schema.org/" aria-label="breadcrumbs"><li property="itemListElement" typeof="ListItem"><a href="/zh-CN/docs/Games" class="breadcrumb" property="item" typeof="WebPage"><span property="name">游戏开发</span></a><meta property="position" content="1"/></li><li property="itemListElement" typeof="ListItem"><a href="/zh-CN/docs/Games/Anatomy" class="breadcrumb-current-page" property="item" typeof="WebPage"><span property="name">剖析游戏结构</span></a><meta property="position" content="2"/></li></ol></nav><div class="article-actions"><button type="button" class="button action has-icon article-actions-toggle" aria-label="Article actions"><span class="button-wrap"><span class="icon icon-ellipses "></span><span class="article-actions-dialog-heading">Article Actions</span></span></button><ul class="article-actions-entries"><li class="article-actions-entry"><div class="languages-switcher-menu open-on-focus-within"><button id="languages-switcher-button" type="button" class="button action small has-icon languages-switcher-menu" aria-haspopup="menu"><span class="button-wrap"><span class="icon icon-language "></span>中文 (简体)</span></button><div class="hidden"><ul class="submenu language-menu " aria-labelledby="language-menu-button"><li class=" "><form class="submenu-item locale-redirect-setting"><div class="group"><label class="switch"><input type="checkbox" name="locale-redirect"/><span class="slider"></span><span class="label">Remember language</span></label><a href="https://github.com/orgs/mdn/discussions/739" rel="external noopener noreferrer" target="_blank" title="Enable this setting to automatically switch to this language when it's available. (Click to learn more.)"><span class="icon icon-question-mark "></span></a></div></form></li><li class=" "><a data-locale="de" href="/de/docs/Games/Anatomy" class="button submenu-item"><span>Deutsch</span><span title="Diese Übersetzung ist Teil eines Experiments."><span class="icon icon-experimental "></span></span></a></li><li class=" "><a data-locale="en-US" href="/en-US/docs/Games/Anatomy" class="button submenu-item"><span>English (US)</span></a></li><li class=" "><a data-locale="es" href="/es/docs/Games/Anatomy" class="button submenu-item"><span>Español</span></a></li><li class=" "><a data-locale="fr" href="/fr/docs/Games/Anatomy" class="button submenu-item"><span>Français</span></a></li><li class=" "><a data-locale="ja" href="/ja/docs/Games/Anatomy" class="button submenu-item"><span>日本語</span></a></li><li class=" "><a data-locale="pt-BR" href="/pt-BR/docs/Games/Anatomy" class="button submenu-item"><span>Português (do Brasil)</span></a></li><li class=" "><a data-locale="ru" href="/ru/docs/Games/Anatomy" class="button submenu-item"><span>Русский</span></a></li></ul></div></div></li></ul></div></div></div></div><div class="container"><div class="notecard localized-content-note"><p><a href="/zh-CN/docs/MDN/Community/Contributing/Translated_content#活跃语言">此页面由社区从英文翻译而来。了解更多并加入 MDN Web Docs 社区。</a></p></div></div><div class="main-wrapper"><div class="sidebar-container"><aside id="sidebar-quicklinks" class="sidebar" data-macro="GamesSidebar"><button type="button" class="button action backdrop" aria-label="Collapse sidebar"><span class="button-wrap"></span></button><nav aria-label="Related Topics" class="sidebar-inner"><header class="sidebar-actions"><section class="sidebar-filter-container"><div class="sidebar-filter "><label id="sidebar-filter-label" class="sidebar-filter-label" for="sidebar-filter-input"><span class="icon icon-filter"></span><span class="visually-hidden">Filter sidebar</span></label><input id="sidebar-filter-input" autoComplete="off" class="sidebar-filter-input-field false" type="text" placeholder="Filter" value=""/><button type="button" class="button action has-icon clear-sidebar-filter-button"><span class="button-wrap"><span class="icon icon-cancel "></span><span class="visually-hidden">Clear filter input</span></span></button></div></section></header><div class="sidebar-inner-nav"><div class="in-nav-toc"><div class="document-toc-container"><section class="document-toc"><header><h2 class="document-toc-heading">在本文中</h2></header><ul class="document-toc-list"><li class="document-toc-item "><a class="document-toc-link" href="#呈现、接收、解释、计算、重复">呈现、接收、解释、计算、重复</a></li><li class="document-toc-item "><a class="document-toc-link" href="#在_javascript_中构建一个主循环">在 JavaScript 中构建一个主循环</a></li><li class="document-toc-item "><a class="document-toc-link" href="#在_javascript_中构建一个更好的主循环">在 JavaScript 中构建一个更好的主循环</a></li><li class="document-toc-item "><a class="document-toc-link" href="#用_javascript_构建一个更优化的主循环">用 JavaScript 构建一个更优化的主循环</a></li><li class="document-toc-item "><a class="document-toc-link" href="#决定……时间">决定……时间</a></li><li class="document-toc-item "><a class="document-toc-link" href="#总结">总结</a></li></ul></section></div></div><div class="sidebar-body"> <ol> <li class="toggle"> <details open=""> <summary>简介</summary> <ol> <li><a href="/zh-CN/docs/Games/Introduction">简介</a></li> <li><em><a href="/zh-CN/docs/Games/Anatomy" aria-current="page">剖析</a></em></li> </ol> </details> </li> <li class="toggle"> <details> <summary>游戏开发用到的 API</summary> <ol> <li><a href="/zh-CN/docs/Web/API/Canvas_API">Canvas</a></li> <li><a href="/zh-CN/docs/Web/CSS">CSS</a></li> <li><a href="/zh-CN/docs/Web/API/Fullscreen_API">全屏</a></li> <li><a href="/zh-CN/docs/Web/API/Gamepad_API">游戏控制器</a></li> <li><a href="/zh-CN/docs/Web/API/IndexedDB_API">IndexedDB</a></li> <li><a href="/zh-CN/docs/Web/JavaScript">JavaScript</a></li> <li><a href="/zh-CN/docs/Web/API/Pointer_Lock_API">指针锁定</a></li> <li><a href="/zh-CN/docs/Web/SVG">SVG</a></li> <li><a href="/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray">类型数组</a></li> <li><a href="/zh-CN/docs/Web/API/Web_Audio_API">Web 音频</a></li> <li><a href="/zh-CN/docs/Web/API/WebGL_API">WebGL</a></li> <li><a href="/zh-CN/docs/Web/API/WebRTC_API">WebRTC</a></li> <li><a href="/zh-CN/docs/Web/API/WebSockets_API">WebSocket</a></li> <li><a href="/zh-CN/docs/Web/API/WebVR_API">WebVR</a></li> <li><a href="/zh-CN/docs/Web/API/Web_Workers_API">Web Worker</a></li> <li><a href="/zh-CN/docs/Web/API/XMLHttpRequest">XMLHttpRequest</a></li> </ol> </details> </li> <li class="toggle"> <details> <summary>技术</summary> <ol> <li><a href="/zh-CN/docs/Games/Techniques/Async_scripts">使用 asm.js 中的异步脚本</a></li> <li><a href="/zh-CN/docs/Web/Performance/Optimizing_startup_performance">优化启动性能</a></li> <li><a class="only-in-en-us" title="此页面目前仅提供英文版本" href="/en-US/docs/Games/Techniques/WebRTC_data_channels">使用 WebRTC 点对点数据通道</a></li> <li><a class="only-in-en-us" title="此页面目前仅提供英文版本" href="/en-US/docs/Games/Techniques/Audio_for_Web_Games">Web 游戏的音频</a></li> <li><a href="/zh-CN/docs/Games/Techniques/2D_collision_detection">2D 碰撞检测</a></li> <li><a class="only-in-en-us" title="此页面目前仅提供英文版本" href="/en-US/docs/Games/Techniques/Tilemaps">平铺和平铺贴图概述</a></li> </ol> </details> </li> <li class="toggle"> <details> <summary>Web 中的 3D 游戏</summary> <ol> <li><a href="/zh-CN/docs/Games/Techniques/3D_on_the_web">概述</a></li> <li><a href="/zh-CN/docs/Games/Techniques/3D_on_the_web/Basic_theory">基本的 3D 理论</a></li> <li><a class="only-in-en-us" title="此页面目前仅提供英文版本" href="/en-US/docs/Games/Techniques/3D_on_the_web/Building_up_a_basic_demo_with_A-Frame">使用 A-Frame 构建基础演示</a></li> <li><a class="only-in-en-us" title="此页面目前仅提供英文版本" href="/en-US/docs/Games/Techniques/3D_on_the_web/Building_up_a_basic_demo_with_Babylon.js">使用 Babylon.js 构建基础演示</a></li> <li><a class="only-in-en-us" title="此页面目前仅提供英文版本" href="/en-US/docs/Games/Techniques/3D_on_the_web/Building_up_a_basic_demo_with_PlayCanvas">使用 PlayCanvas 构建基础演示</a></li> <li><a href="/zh-CN/docs/Games/Techniques/3D_on_the_web/Building_up_a_basic_demo_with_Three.js">使用 Three.js 构建基础演示</a></li> <li><a class="only-in-en-us" title="此页面目前仅提供英文版本" href="/en-US/docs/Games/Techniques/3D_on_the_web/WebXR">WebXR</a></li> <li><a href="/zh-CN/docs/Games/Techniques/3D_collision_detection">3D 碰撞检测</a></li> <li><a class="only-in-en-us" title="此页面目前仅提供英文版本" href="/en-US/docs/Games/Techniques/3D_collision_detection/Bounding_volume_collision_detection_with_THREE.js">使用 THREE.js 进行边界体积碰撞检测</a></li> </ol> </details> </li> <li class="toggle"> <details> <summary>实现游戏控制机制</summary> <ol> <li><a href="/zh-CN/docs/Games/Techniques/Control_mechanisms">控制机制</a></li> <li><a href="/zh-CN/docs/Games/Techniques/Control_mechanisms/Mobile_touch">移动端触摸</a></li> <li><a class="only-in-en-us" title="此页面目前仅提供英文版本" href="/en-US/docs/Games/Techniques/Control_mechanisms/Desktop_with_mouse_and_keyboard">通过鼠标和键盘控制的桌面端</a></li> <li><a class="only-in-en-us" title="此页面目前仅提供英文版本" href="/en-US/docs/Games/Techniques/Control_mechanisms/Desktop_with_gamepad">通过游戏手柄控制的桌面端</a></li> <li><a class="only-in-en-us" title="此页面目前仅提供英文版本" href="/en-US/docs/Games/Techniques/Control_mechanisms/Other">其他</a></li> </ol> </details> </li> <li class="toggle"> <details> <summary>教程</summary> <ol> <li><a href="/zh-CN/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript">使用纯 JavaScript 实现 2D 打砖块游戏</a></li> <li><a href="/zh-CN/docs/Games/Tutorials/2D_breakout_game_Phaser">使用 Phaser 实现 2D 打砖块游戏</a></li> <li><a class="only-in-en-us" title="此页面目前仅提供英文版本" href="/en-US/docs/Games/Tutorials/HTML5_Gamedev_Phaser_Device_Orientation">具有设备方向检测功能的 2D 迷宫游戏</a></li> <li><a href="https://mozdevs.github.io/html5-games-workshop/en/guides/platformer/start-here/">使用 Phaser 实现 2D 平板游戏</a></li> </ol> </details> </li> <li class="toggle"> <details> <summary>发布游戏</summary> <ol> <li><a href="/zh-CN/docs/Games/Publishing_games">概述</a></li> <li><a href="/zh-CN/docs/Games/Publishing_games/Game_distribution">游戏分发</a></li> <li><a href="/zh-CN/docs/Games/Publishing_games/Game_promotion">游戏推广</a></li> <li><a href="/zh-CN/docs/Games/Publishing_games/Game_monetization">游戏盈利</a></li> </ol> </details> </li> </ol> </div></div><section class="place side"></section></nav></aside><div class="toc-container"><aside class="toc"><nav><div class="document-toc-container"><section class="document-toc"><header><h2 class="document-toc-heading">在本文中</h2></header><ul class="document-toc-list"><li class="document-toc-item "><a class="document-toc-link" href="#呈现、接收、解释、计算、重复">呈现、接收、解释、计算、重复</a></li><li class="document-toc-item "><a class="document-toc-link" href="#在_javascript_中构建一个主循环">在 JavaScript 中构建一个主循环</a></li><li class="document-toc-item "><a class="document-toc-link" href="#在_javascript_中构建一个更好的主循环">在 JavaScript 中构建一个更好的主循环</a></li><li class="document-toc-item "><a class="document-toc-link" href="#用_javascript_构建一个更优化的主循环">用 JavaScript 构建一个更优化的主循环</a></li><li class="document-toc-item "><a class="document-toc-link" href="#决定……时间">决定……时间</a></li><li class="document-toc-item "><a class="document-toc-link" href="#总结">总结</a></li></ul></section></div></nav></aside><section class="place side"></section></div></div><main id="content" class="main-content "><article class="main-page-content" lang="zh-CN"><header><h1>剖析游戏结构</h1></header><div class="section-content"><p>本文从技术角度分析了一般电子游戏的结构和工作流程,就此介绍主循环是如何运行的。它有助于初学者了解在现代游戏开发领域构建游戏时需要什么,以及理解像 JavaScript 这样的 Web 标准是如何成为可用于开发游戏的工具的。游戏开发经验丰富但不熟悉 Web 开发的开发者也能从本文中受益。</p></div><section aria-labelledby="呈现、接收、解释、计算、重复"><h2 id="呈现、接收、解释、计算、重复"><a href="#呈现、接收、解释、计算、重复">呈现、接收、解释、计算、重复</a></h2><div class="section-content"><p>所有电子游戏的目标都是向用户<strong>呈现</strong>一个场景,<strong>接收</strong>他们的输入,将这些输入信号<strong>解释</strong>为动作,并<strong>计算</strong>出由这些动作产生的新场景。游戏不断地循环遍历,一遍又一遍,直到遇到某些终止条件(比如赢、输或者用户退出休息)。毫不奇怪,这种模式与游戏引擎的编程方式相呼应。</p> <p>具体情况取决于游戏本身。</p> <p>一些游戏通过<strong>用户输入</strong>来驱动这个循环。想象一下,你正在开发一种“<em>找不同</em>”类型的游戏。这些游戏向用户<strong>呈现</strong>两张图片、<strong>接收</strong>用户的点击(或触摸)、将用户输入<strong>解释</strong>为成功、失败、暂停、菜单交互等。最后,游戏根据用户的输入<strong>计算</strong>得到新的游戏场景。这种游戏是由用户的输入驱动,也就是说,它会在用户进行输入之后冻结画面,等待玩家进行新的输入。这是一种基于回合的游戏类型,它不需要每帧持续更新画面,只有当玩家作出反应时才会。</p> <p>另一些游戏需要尽可能控制每一个细微的时间片段。与上述原理有点小区别:每个动画帧都将循环遍历,并在之后第一个可用的轮次捕获玩家输入的任何变化。这种每帧一次的模型是通过一个叫<strong>主循环</strong>的东西实现的。如果你的游戏循环是基于时间的,则必须保证对主循环精准的模拟。</p> <p>但它也可能不需要逐帧控制。你的游戏循环可能类似于<em>找不同</em>的例子,并且以输入事件作为基础,那么它可能同时需要输入和模拟时间片段。它甚至可以基于其他的东西来循环。</p> <p>正如我们将再下一节中所描述的,现代 JavaScript 可以轻松开发出一个高效的,逐帧执行的主循环。当然,你的游戏只会按照你所做的那样优化。如果某些东西看起来应该被添加到一个更少发生的事件里,那么将它从主循环中剥离出来通常是个好主意(但并非总是如此)。</p></div></section><section aria-labelledby="在_javascript_中构建一个主循环"><h2 id="在_javascript_中构建一个主循环"><a href="#在_javascript_中构建一个主循环">在 JavaScript 中构建一个主循环</a></h2><div class="section-content"><p>JavaScript 能很好的处理事件和回调函数。现代浏览器努力在需要的时候调用方法,并在间隙中闲下来(或做其他任务)。将你的代码附加到适合它们的时刻是一个很好的主意。考虑一下你的函数是需要在严格的时间周期内,还是每一帧,或者仅仅是在发生了其他情况之后执行。当你的函数需要被调用时,要更具体地使用浏览器,这样浏览器就可以在调用时进行优化。而且,这可能会让你的工作更轻松。</p> <p>有些代码需要逐帧运行,所以应将其附加到浏览器的重绘周期中,没有比这更好的了!在 Web 中通常将 <a href="/zh-CN/docs/Web/API/Window/requestAnimationFrame"><code>window.requestAnimationFrame()</code></a> 方法作为大多数良好的逐帧循环的基础。在调用该方法时必须传入一个回调函数,这个回调函数将在下一次重新绘制之前执行。下面是一个简单的主循环的例子:</p> <div class="code-example"><div class="example-header"><span class="language-name">js</span></div><pre class="brush: js notranslate"><code>window.main = () => { window.requestAnimationFrame(main); // 你的主循环可以做你想要做的任何事情 }; main(); // 开始主循环 </code></pre></div> <div class="notecard note"> <p><strong>备注:</strong>在这里讨论的每个 <code>main()</code> 方法中,在执行循环内容之前,我们会递归调用一个新的 <code>requestAnimationFrame</code>。这不是毫无根据而写的:这样做被认为是最佳实践。如果你的当前帧错过了它的垂直同步窗口的周期,你也在下一个帧通过 <code>requestAnimationFrame</code> 尽早的调用 <code>main()</code>,从而确保浏览器能够及时地执行。</p> </div> <p>上面的代码块有两个语句。第一个语句创建一个名为 <code>main()</code> 的全局变量的函数。这个函数会执行一些操作,同时告诉浏览器在下一帧通过 <code>window.requestAnimationFrame()</code> 调用本身。第二个语句调用第一个语句中定义的 <code>main()</code> 函数。因为 <code>main()</code> 中在第二个语句中被调用一次,而每次调用都将自身又放置到下一个帧的执行队列中,因此 <code>main()</code> 的调用与你的帧率同步。</p> <p>当然,这个循环并不完美。不过在讨论如何改善之前,让我们先讨论一下它已经做到了什么。</p> <p>将主循环的时机安排在浏览器每次的绘制显示中,允许你能像浏览器想要绘制的那样频繁的运行你的循环。你能够控制每一帧动画,并且 <code>main()</code> 是循环中唯一执行的函数,所以这很简单。主视角射击游戏(或类似的游戏)每一帧都会出现一个新的场景。没有比这种方法更平滑并绘制及时的了。</p> <p>但是不要马上认为动画必需要帧帧控制。通过 CSS 动画或浏览器中的其他工具,我们也可以很容易实现简单的动画,甚至能用上 GPU 加速。还有有很多类似这样的工具能让我们的开发变得更加简单。</p></div></section><section aria-labelledby="在_javascript_中构建一个更好的主循环"><h2 id="在_javascript_中构建一个更好的主循环"><a href="#在_javascript_中构建一个更好的主循环">在 JavaScript 中构建一个更好的主循环</a></h2><div class="section-content"><p>在前面的主循环中有两个明显的问题:<code>main()</code> 污染了 <a href="/zh-CN/docs/Web/API/Window"><code>window</code></a> 对象(所有全局变量存储的对象),并且示例代码没有给我们留下一个<em>停止</em>循环的方法(除非整个标签页被关闭或刷新)。对于第一个问题,如果你希望主循环只运行,并且不需要被简单(直接)地访问,你可以将它创建为一个立即调用的函数表达式(IIFE)。</p><!-- prettier-ignore-start --> <div class="code-example"><div class="example-header"><span class="language-name">js</span></div><pre class="brush: js notranslate"><code>/* * 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入(ASI)。 * 浏览器可能会意外地认为整个示例会从上一行继续。 * 如果前一行不为空或终止,则前面的分号标志着新行的开始。 */ ;(() => { function main(){ window.requestAnimationFrame(main); // 你的主循环内容。 } main(); // 开始循环 })(); </code></pre></div><!-- prettier-ignore-end --> <p>当浏览器遇到这个 IIFE 时,它将定义你的主循环,并立即将其加入下一个帧的更新队列中。<code>main</code>(或作为方法而言的 <code>main()</code>)不会被附加到任何对象。它在应用程序的其他地方仍是一个有效的未使用的名称,仍可以自由地被定义为其他的东西。</p> <div class="notecard note"> <p><strong>备注:</strong>在实践中,更常见的终止下一个 <code>requestAnimationFrame()</code> 方式是使用 if 语句,而不是调用 <code>cancelAnimationFrame()</code>。</p> </div> <p>对于第二个问题,要终止循环,你需要调用 <a href="/zh-CN/docs/Web/API/Window/cancelAnimationFrame"><code>window.cancelAnimationFrame()</code></a> 来取消 <code>main()</code> 的调用。该方法需要传入你最后一次调用 <code>requestAnimationFrame()</code> 时返回的 ID。让我们假设你的游戏的函数和变量是建立在你称为 <code>MyGame</code> 的名称空间上。扩展我们的最后一个例子,主循环现在看起来是这样的:</p><!-- prettier-ignore-start --> <div class="code-example"><div class="example-header"><span class="language-name">js</span></div><pre class="brush: js notranslate"><code>/* * 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入(ASI)。 * 浏览器可能会意外地认为整个示例会从上一行继续。 * 如果前一行不为空或终止,则前面的分号标志着新行的开始。 * * 让我们假设 MyGame 是之前定义的。 */ ;(() => { function main(){ MyGame.stopMain = window.requestAnimationFrame(main); // 你的主循环内容 } main(); // 开始循环 })(); </code></pre></div><!-- prettier-ignore-end --> <p>现在,我们在 <code>MyGame</code> 名称空间中声明了一个变量 <code>stopMain</code>,其值为主循环最后调用 <code>requestAnimationFrame()</code> 时返回的 ID。任何时候,我们可以通过告诉浏览器取消与 ID 相对应的请求来停止主循环。</p> <div class="code-example"><div class="example-header"><span class="language-name">js</span></div><pre class="brush: js notranslate"><code>window.cancelAnimationFrame(MyGame.stopMain); </code></pre></div> <p>在 JavaScript 的中编写主循环的关键在于考虑到任何会驱动你行为的事件,同时要注意不同的系统是如何相互作用的。你可能拥有多个由多个不同类型的事件驱动的组件。这看起来像是不必要地变复杂了,但它可能也是一个很好的优化(当然也不一定)。问题是,你并不是在编写一个典型的主循环。在 JavaScript 中,你使用的是浏览器的主循环,并且你正在尝试这样做。</p></div></section><section aria-labelledby="用_javascript_构建一个更优化的主循环"><h2 id="用_javascript_构建一个更优化的主循环"><a href="#用_javascript_构建一个更优化的主循环">用 JavaScript 构建一个更优化的主循环</a></h2><div class="section-content"><p>基本上,在 JavaScript 的中,浏览器有它自己的主循环,而你的代码存在于循环某些阶段。上面描述的主循环,试图避免脱离浏览器的控制。这种主循环附着于 <code>window.requestAnimationFrame()</code> 方法,该方法将在浏览器的下一帧中执行,具体取决于浏览器如何与将其自己的主循环关联起来。<a href="https://www.w3.org/TR/animation-timing/" class="external" target="_blank">W3C 的 requestAnimationFrame 规范</a>并没有真正定义什么时候浏览器必须执行 requestAnimationFrame 回调。这有一个好处,浏览器厂商可以自由地实现他们认为最好的解决方案,并随着时间的推移进行调整。</p> <p>现代版的 Firefox 和 Google Chrome(可能还有其他浏览器)都尝试图在框架的时间片段的开始时<em>尝试</em>将 <code>requestAnimationFrame</code> 回调与它们的主线程进行连接。因此,浏览器的主线程<em>看起来</em>就像下面这样:</p> <ol> <li>启动一个新帧(而之前的帧由显示器处理)。</li> <li>遍历 <code>requestAnimationFrame</code> 回调列表并调用它们。</li> <li>当上面的回调停止控制主线程时,执行垃圾收集和其他帧任务。</li> <li>睡眠(除非事件打断了浏览器的睡眠),直到显示器准备好你的图像(<a href="https://www.techopedia.com/definition/92/vertical-sync-vsync" class="external" target="_blank">垂直同步</a>)并重复。</li> </ol> <p>你可以考虑开发实时应用程序,因为有时间做工作。所有上述步骤都必须在每 16 毫秒内进行一次,以跟上 60Hz 的显示器。浏览器会尽可能早地调用你的代码,从而给它最大的计算时间。你的主线程通常会启动一些甚至不在主线程上的工作负载(如 WebGL 的中的光栅化或着色器)。在浏览器使用其主线程管理垃圾收集,其他任务或处理异步事件时,可以在 Web Worker 或 GPU 上执行长时间的计算。</p> <p>当我们讨论预算时间时,许多 Web 浏览器都有一个称为<em>高解析度时间</em>的工具。<a href="/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date"><code>Date</code></a> 对象不再是公认的事件计时方法,因为它非常不精确,可以由系统时钟进行修改。另一方面,高解析度时间计算自 <code>navigationStart</code>(当上一个文档被卸载时)的毫秒数。这个值以小数的精度返回,精确到千分之一毫秒。它被称为 <a href="/zh-CN/docs/Web/API/DOMHighResTimeStamp"><code>DOMHighResTimeStamp</code></a>,但是,无论出于什么意图和目的,都认为它是一个浮点数。</p> <div class="notecard note"> <p><strong>备注:</strong>系统(硬件或软件)不能达到微秒精度,则至少提供毫秒级的精度。不过,如果能够达到微秒的精度,那就应该提供这一精度。</p> </div> <p>这个值本身并不太有用,因为它与一个相当无趣的事件相关,但它可以从另一个时间戳中减去,以便准确准确地确定这两个点之间的时间间隔。要获得这些时间戳中的一个,你可以调用 <code>window.performance.now()</code> 并将结果存储为一个变量。</p> <div class="code-example"><div class="example-header"><span class="language-name">js</span></div><pre class="brush: js notranslate"><code>const tNow = window.performance.now(); </code></pre></div> <p>回到主循环的主题。你将经常想知道何时调用主函数。因为这是常见的,<code>window.requestAnimationFrame()</code> 总是在执行回调函数时提供 <code>DOMHighResTimeStamp</code> 作为其参数。这将为我们之前的主循环带来另一个增强。</p><!-- prettier-ignore-start --> <div class="code-example"><div class="example-header"><span class="language-name">js</span></div><pre class="brush: js notranslate"><code>/* * 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入(ASI)。 * 浏览器可能会意外地认为整个示例会从上一行继续。 * 如果前一行不为空或终止,则前面的分号标志着新行的开始。 * * 我们还假设 MyGame 是以前定义的。 */ ;(() => { function main(tFrame){ MyGame.stopMain = window.requestAnimationFrame(main); // 你的主循环内容 // tFrame,来源于“function main(tFrame)”,现在变为是由 rAF 提供的 DOMHighResTimeStamp。 } main(); // 开始循环 })(); </code></pre></div><!-- prettier-ignore-end --> <p>其他一些优化是可能的,这取决于你的游戏想要完成什么。你的游戏类型显然会有所不同,但它甚至可能比这更微妙。你可以在画布上单独绘制每个像素,也可以将 DOM 元素(包括具有透明背景的多个 WebGL 的画布)放入复杂的层次结构中。每条路径都将带来不同的机会和约束。</p></div></section><section aria-labelledby="决定……时间"><h2 id="决定……时间"><a href="#决定……时间">决定……时间</a></h2><div class="section-content"><p>你将需要对你的主循环作出艰难的决定:如何模拟准确的时间进度。如果你想要控制每一帧,那么你需要确定你的游戏更新和绘制的频率,你甚至可能希望以不同的速率进行更新和绘制。你还需要考虑如果用户的系统无法跟上工作负载,那么你还需考虑如何优雅降级,以保证性能。让我们首先假定你会在每次绘制时,同时处理用户的输入,并更新游戏状态。我们稍后再细讲。</p> <div class="notecard note"> <p><strong>备注:</strong>改变主循环如何处理时间是非常困难的,在进行主循环之前,请仔细考虑你的需求。</p> </div></div></section><section aria-labelledby="大多数浏览器游戏应该是什么样的"><h3 id="大多数浏览器游戏应该是什么样的"><a href="#大多数浏览器游戏应该是什么样的">大多数浏览器游戏应该是什么样的</a></h3><div class="section-content"><p>如果你的游戏可以达到你所支持的任何硬件的最大刷新率,那么你的工作就变得相当容易了。你可以简单地进行更新、渲染,然后在垂直同步之前什么都不用做。</p><!-- prettier-ignore-start --> <div class="code-example"><div class="example-header"><span class="language-name">js</span></div><pre class="brush: js notranslate"><code>/* * 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入(ASI)。 * 浏览器可能会意外地认为整个示例从上一行继续。 * 如果前一行不为空或终止,则前面的分号标志着新行的开始。 * * 我们还假设 MyGame 是以前定义的。 */ ;(() => { function main(tFrame) { MyGame.stopMain = window.requestAnimationFrame(main); update(tFrame); // 调用 update 方法。在我们的例子中,我们给它 rAF 的时间戳。 render(); } main(); // 开始循环 })(); </code></pre></div><!-- prettier-ignore-end --> <p>如果无法达到最大刷新率,可以调整画面质量设置以保持你的时间预算。这个概念最有名的例子是 id Software 的游戏——狂怒。这个游戏取消了用户的控制权,以使其计算时间保持在大约 16ms(或大约 60fps)。如果计算时间过长,渲染的分辨率就会降低,纹理和其他资源将无法加载或绘制等。这个(非 Web 的)案例研究做了一些假设和折衷:</p> <ul> <li>每个动画帧都考虑用户输入。</li> <li>没有帧需要外推(猜测),因为每个绘图都有自己的更新。</li> <li>仿真系统基本上可以假定每次完全更新间隔约 16ms。</li> <li>给用户控制质量设置将是一场噩梦。</li> <li>不同的监视器以不同的速率输入:30FPS、75FPS、100FPS、120FPS、144FPS 等</li> <li>无法跟上 60 FPS 的系统会失去视觉质量,以保持游戏运行的最佳速度(最终如果质量太低,则会是彻底的失败)。</li> </ul></div></section><section aria-labelledby="处理可变刷新率需求的其他方法"><h3 id="处理可变刷新率需求的其他方法"><a href="#处理可变刷新率需求的其他方法">处理可变刷新率需求的其他方法</a></h3><div class="section-content"><p>存在其他解决问题的方法。</p> <p>一种常见的技术是以恒定的频率更新模拟,然后绘制尽可能多的(或尽可能少的)实际帧。更新方法可以继续循环,而不用考虑用户看到的内容。绘图方法可以查看最后的更新以及发生的时间。由于绘制知道何时表示,以及上次更新的模拟时间,它可以预测为用户绘制一个合理的框架。这是否比官方更新循环更频繁(甚至更不频繁)无关紧要。更新方法设置检查点,并且像系统允许的那样频繁地,渲染方法画出周围的时间。在 Web 标准中分离更新有很多种方法:</p> <ul> <li> <p>在 <code>requestAnimationFrame()</code> 中绘制,并在 <a href="/zh-CN/docs/Web/API/Window/setInterval" title="setInterval()"><code>setInterval()</code></a> 或 <a href="/zh-CN/docs/Web/API/Window/setTimeout" title="setTimeout()"><code>setTimeout()</code></a> 中更新。</p> <ul> <li>即使在未聚焦或最小化的情况下,使用处理器时间,也可能是主线程,并且可能是传统游戏循环的工件(但是很简单)。</li> </ul> </li> <li> <p>在 <code>requestAnimationFrame()</code> 中绘制,并在 <a href="/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers">Web Worker</a> 的 <a href="/zh-CN/docs/Web/API/WorkerGlobalScope/setInterval" title="setInterval()"><code>setInterval()</code></a> 或 <a href="/zh-CN/docs/Web/API/WorkerGlobalScope/setTimeout" title="setTimeout()"><code>setTimeout()</code></a> 中对其进行更新。</p> <ul> <li>这与上述相同,除了更新不会使主线程(主线程也没有)。这是一个更复杂的解决方案,并且对于简单更新可能会有太多的开销。</li> </ul> </li> <li> <p>在 <code>requestAnimationFrame()</code> 中绘制,并使用它来戳一个包含更新方法的 Web Worker,其中包含要计算的刻度数(如果有的话)。</p> <ul> <li>这个睡眠直到 <code>requestAnimationFrame()</code> 被调用并且不会污染主线程,加上你不依赖于老式的方法。再次,这比以前的两个选项更复杂一些,并且<em>开始</em>每个更新将被阻止,直到浏览器决定启动 rAF 回调。</li> </ul> </li> </ul> <p>这些方法中的每一种都有类似的权衡:</p> <ul> <li>用户可以跳过渲染帧或根据其性能插入额外的帧。</li> <li>你可以指望所有用户以相同的固定频率更新非修饰性的变量,而不会卡顿。</li> <li>程序比我们前面看到的基本循环要复杂得多。</li> <li>用户输入完全被忽略,直到下次更新(即使用户具有快速设备)。</li> <li>强制插帧具有性能损失。</li> </ul> <p>单独的更新和绘图方法可能类似于下面的示例。为了演示,该示例基于第三点,只是没有使用 Web Worker 以提高可读性(老实说,也包含可写性)。</p> <div class="notecard warning"> <p><strong>警告:</strong>这个例子需要进行单独的技术审查。</p> </div><!-- prettier-ignore-start --> <div class="code-example"><div class="example-header"><span class="language-name">js</span></div><pre class="brush: js notranslate"><code>/* * 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入(ASI)。 * 浏览器可能会意外地认为整个示例从上一行继续。 * 如果前一行不为空或终止,则前面的分号标志着新行的开始。 * * 我们还假设 MyGame 是以前定义的。 * * MyGame.lastRender 跟踪上一次提供的 requestAnimationFrame 时间戳。 * MyGame.lastTick 跟踪上次更新时间。始终以 tickLength 递增。 * MyGame.tickLength 是游戏状态更新的频率。这里是 20 Hz(50ms)。 * * timeSinceTick 是 requestAnimationFrame 回调和上一次更新之间的时间。 * numTicks 是这两个呈现帧之间应该发生的更新次数。 * * render() 传入 tFrame, 因为 render 方法可能需要计算 * tFrame 距离最近的更新已经过去了多久,通过外推的方式 * 来获得场景数据。(对于快速设备,render 方法是纯装饰性的)。 * 用以绘制场景。 * * update() 根据给定时间点计算游戏状态。通常需要用 tickLength * 作为循环参数,递增更新。来保证游戏状态的严谨。传入 DOMHighResTimeStamp * 代表当前时间。(除非需要增加暂停功能,传入的时间应该总是 * 上次更新时间 + 游戏的 tick 间隔。) * * setInitialState() 执行在运行主循环之前需要的任何任务。 * 它只是一个你可能添加的通用示例函数。 */ ;(() => { function main(tFrame) { MyGame.stopMain = window.requestAnimationFrame(main); const nextTick = MyGame.lastTick + MyGame.tickLength; let numTicks = 0; // 如果 tFrame < nextTick,则需要更新 0 个 tick(对于 numTicks,默认为 0)。 // 如果 tFrame = nextTick,则需要更新 1 tick(等等)。 // 备注:正如我们在总结中提到的那样,你应该跟踪 numTicks 的大小。 // 如果它很大,要么你的游戏是卡住了,要么机器无法跟上。 if (tFrame > nextTick) { const timeSinceTick = tFrame - MyGame.lastTick; numTicks = Math.floor(timeSinceTick / MyGame.tickLength); } queueUpdates(numTicks); render(tFrame); MyGame.lastRender = tFrame; } function queueUpdates(numTicks) { for (let i = 0; i < numTicks; i++) { MyGame.lastTick += MyGame.tickLength; // 现在 lastTick 应是这一时间。 update(MyGame.lastTick); } } MyGame.lastTick = performance.now(); MyGame.lastRender = MyGame.lastTick; // 假装第一次绘制是在第一次更新。 MyGame.tickLength = 50; // 这将使你的模拟运行在 20Hz(50ms) setInitialState(); main(performance.now()); // 开始循环 })(); </code></pre></div><!-- prettier-ignore-end --> <p>另一个选择是简单地做一些事情不那么频繁。如果你的更新循环的一部分难以计算但对时间不敏感,则可以考虑缩小其频率,理想情况下,在延长的时间段内将其扩展成块。这是一个隐含的例子,在火炮博物馆的炮兵游戏中,他们<a href="https://web.archive.org/web/20161021030645/http://blog.artillery.com/2012/10/browser-garbage-collection-and-framerate.html" class="external" target="_blank">调整垃圾发生率</a>来优化垃圾回收。显然,清理资源不是时间敏感的(特别是如果整理比垃圾本身更具破坏性)。</p> <p>这也可能适用于你自己的一些任务。那些是当可用资源成为关注点时的好候选人。</p></div></section><section aria-labelledby="总结"><h2 id="总结"><a href="#总结">总结</a></h2><div class="section-content"><p>我知道上述的任何一种,或许没有适合你的游戏。正确的决定完全取决于你愿意(和不愿意)做出的权衡。主要关心的是切换到另一个选项。幸运的是,我没有任何经验,但我听说这是一个令人痛苦的打地鼠游戏。</p> <p>像 Web 这样的受管理平台,要记住的一件重要的事情是,你的循环可能会在相当长的一段时间内停止执行。当用户取消选择你的标签并且浏览器休眠(或减慢)其 <code>requestAnimationFrame</code> 回调间隔时,可能会发生这种情况。你有很多方法来处理这种情况,这可能取决于你的游戏是单人游戏还是多人游戏。一些选择是:</p> <ul> <li> <p>考虑差距“暂停”并跳过时间。</p> <ul> <li>你可能会看到这对大多数多人游戏来说都是有问题的。</li> </ul> </li> <li> <p>你可以模拟差距赶上。</p> <ul> <li>这可能是长时间丢失和/或复杂更新的问题。</li> </ul> </li> <li> <p>你可以从联机设备或服务器恢复游戏状态。</p> <ul> <li>除非联机设备/服务器的游戏状态已经过期,或者这是一个没有没服务器的纯单机游戏。</li> </ul> </li> </ul> <p>一旦你的主循环被开发出来,你已经决定了一套适合你的游戏的假设和权衡,现在只需要用你的决定来计算任何适用的物理、AI、声音、网络同步,以及其他任何你的游戏可能需要的。</p></div></section></article><aside class="article-footer"><div class="article-footer-inner"><div class="svg-container"><svg xmlns="http://www.w3.org/2000/svg" width="162" height="162" viewBox="0 0 162 162" fill="none" role="none"><mask id="b" fill="#fff"><path d="M97.203 47.04c8.113-7.886 18.004-13.871 28.906-17.492a78 78 0 0 1 33.969-3.39c11.443 1.39 22.401 5.295 32.024 11.411s17.656 14.28 23.476 23.86c5.819 9.579 9.269 20.318 10.083 31.385a69.85 69.85 0 0 1-5.387 32.44c-4.358 10.272-11.115 19.443-19.747 26.801-8.632 7.359-18.908 12.709-30.034 15.637l-6.17-21.698c7.666-2.017 14.746-5.703 20.694-10.773 5.948-5.071 10.603-11.389 13.606-18.467a48.14 48.14 0 0 0 3.712-22.352c-.561-7.625-2.938-15.025-6.948-21.625s-9.544-12.226-16.175-16.44-14.181-6.904-22.065-7.863a53.75 53.75 0 0 0-23.405 2.336c-7.513 2.495-14.327 6.62-19.918 12.053z"></path></mask><path stroke="url(#a)" stroke-dasharray="6, 6" stroke-width="2" d="M97.203 47.04c8.113-7.886 18.004-13.871 28.906-17.492a78 78 0 0 1 33.969-3.39c11.443 1.39 22.401 5.295 32.024 11.411s17.656 14.28 23.476 23.86c5.819 9.579 9.269 20.318 10.083 31.385a69.85 69.85 0 0 1-5.387 32.44c-4.358 10.272-11.115 19.443-19.747 26.801-8.632 7.359-18.908 12.709-30.034 15.637l-6.17-21.698c7.666-2.017 14.746-5.703 20.694-10.773 5.948-5.071 10.603-11.389 13.606-18.467a48.14 48.14 0 0 0 3.712-22.352c-.561-7.625-2.938-15.025-6.948-21.625s-9.544-12.226-16.175-16.44-14.181-6.904-22.065-7.863a53.75 53.75 0 0 0-23.405 2.336c-7.513 2.495-14.327 6.62-19.918 12.053z" mask="url(#b)" style="stroke:url(#a)" transform="translate(-63.992 -25.587)"></path><ellipse cx="8.066" cy="111.597" fill="var(--background-tertiary)" rx="53.677" ry="53.699" transform="matrix(.71707 -.697 .7243 .6895 0 0)"></ellipse><g clip-path="url(#c)" transform="translate(-63.992 -25.587)"><path fill="#9abff5" d="m144.256 137.379 32.906 12.434a4.41 4.41 0 0 1 2.559 5.667l-9.326 24.679a4.41 4.41 0 0 1-5.667 2.559l-8.226-3.108-2.332 6.17c-.466 1.233-.375 1.883-1.609 1.417l-2.253-.527c-.411-.155-.95-.594-1.206-1.161l-4.734-10.484-12.545-4.741a4.41 4.41 0 0 1-2.559-5.667l9.325-24.679a4.41 4.41 0 0 1 5.667-2.559m9.961 29.617 8.227 3.108 3.264-8.638-.498-6.768-4.113-1.555.548 7.258-4.319-1.632zm-12.339-4.663 8.226 3.108 3.264-8.637-.498-6.769-4.113-1.554.548 7.257-4.319-1.632z"></path></g><g clip-path="url(#d)" transform="translate(-63.992 -25.587)"><path fill="#81b0f3" d="M135.35 60.136 86.67 41.654c-3.346-1.27-7.124.428-8.394 3.775L64.414 81.938c-1.27 3.347.428 7.125 3.774 8.395l12.17 4.62-3.465 9.128c-.693 1.826-1.432 2.457.394 3.15l3.014 1.625c.609.231 1.637.274 2.477-.104l15.53-6.983 18.56 7.047c3.346 1.27 7.124-.428 8.395-3.775l13.862-36.51c1.27-3.346-.428-7.124-3.775-8.395M95.261 83.207l-12.17-4.62 4.852-12.779 7.19-7.017 6.085 2.31-7.725 7.51 6.389 2.426zm18.255 6.93-12.17-4.62 4.852-12.778 7.189-7.017 6.085 2.31-7.725 7.51 6.39 2.426z"></path></g><defs><clipPath id="c"><path fill="#fff" d="m198.638 146.586-65.056-24.583-24.583 65.057 65.056 24.582z"></path></clipPath><clipPath id="d"><path fill="#fff" d="m66.438 14.055 96.242 36.54-36.54 96.243-96.243-36.54z"></path></clipPath><linearGradient id="a" x1="97.203" x2="199.995" y1="47.04" y2="152.793" gradientUnits="userSpaceOnUse"><stop stop-color="#086DFC"></stop><stop offset="0.246" stop-color="#2C81FA"></stop><stop offset="0.516" stop-color="#5497F8"></stop><stop offset="0.821" stop-color="#80B0F6"></stop><stop offset="1" stop-color="#9ABFF5"></stop></linearGradient></defs></svg></div><h2>Help improve MDN</h2><fieldset class="feedback"><label>Was this page helpful to you?</label><div class="button-container"><button type="button" class="button primary has-icon yes"><span class="button-wrap"><span class="icon icon-thumbs-up "></span>Yes</span></button><button type="button" class="button primary has-icon no"><span class="button-wrap"><span class="icon icon-thumbs-down "></span>No</span></button></div></fieldset><a class="contribute" href="https://github.com/mdn/translated-content/blob/main/CONTRIBUTING.md" title="This will take you to our contribution guidelines on GitHub." target="_blank" rel="noopener noreferrer">Learn how to contribute</a>.<p class="last-modified-date">This page was last modified on<!-- --> <time dateTime="2024-11-24T15:24:16.000Z">2024年11月24日</time> by<!-- --> <a href="/zh-CN/docs/Games/Anatomy/contributors.txt" rel="nofollow">MDN contributors</a>.</p><div id="on-github" class="on-github"><a href="https://github.com/mdn/translated-content/blob/main/files/zh-cn/games/anatomy/index.md?plain=1" title="Folder: zh-cn/games/anatomy (Opens in a new tab)" target="_blank" rel="noopener noreferrer">View this page on GitHub</a> <!-- -->•<!-- --> <a href="https://github.com/mdn/translated-content/issues/new?template=page-report-zh-cn.yml&mdn-url=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FGames%2FAnatomy&metadata=%3C%21--+Do+not+make+changes+below+this+line+--%3E%0A%3Cdetails%3E%0A%3Csummary%3EPage+report+details%3C%2Fsummary%3E%0A%0A*+Folder%3A+%60zh-cn%2Fgames%2Fanatomy%60%0A*+MDN+URL%3A+https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FGames%2FAnatomy%0A*+GitHub+URL%3A+https%3A%2F%2Fgithub.com%2Fmdn%2Ftranslated-content%2Fblob%2Fmain%2Ffiles%2Fzh-cn%2Fgames%2Fanatomy%2Findex.md%0A*+Last+commit%3A+https%3A%2F%2Fgithub.com%2Fmdn%2Ftranslated-content%2Fcommit%2Fd0d7afe1fb548a66e5ad2ed50e43afbb00114b67%0A*+Document+last+modified%3A+2024-11-24T15%3A24%3A16.000Z%0A%0A%3C%2Fdetails%3E" title="This will take you to GitHub to file a new issue." target="_blank" rel="noopener noreferrer">Report a problem with this content</a></div></div></aside></main></div></div><footer id="nav-footer" class="page-footer"><div class="page-footer-grid"><div class="page-footer-logo-col"><a href="/" class="mdn-footer-logo" aria-label="MDN homepage"><svg width="48" height="17" viewBox="0 0 48 17" fill="none" xmlns="http://www.w3.org/2000/svg"><title id="mdn-footer-logo-svg">MDN logo</title><path d="M20.04 16.512H15.504V10.416C15.504 9.488 15.344 8.824 15.024 8.424C14.72 8.024 14.264 7.824 13.656 7.824C12.92 7.824 12.384 8.064 12.048 8.544C11.728 9.024 11.568 9.64 11.568 10.392V14.184H13.008V16.512H8.472V10.416C8.472 9.488 8.312 8.824 7.992 8.424C7.688 8.024 7.232 7.824 6.624 7.824C5.872 7.824 5.336 8.064 5.016 8.544C4.696 9.024 4.536 9.64 4.536 10.392V14.184H6.6V16.512H0V14.184H1.44V8.04H0.024V5.688H4.536V7.32C5.224 6.088 6.32 5.472 7.824 5.472C8.608 5.472 9.328 5.664 9.984 6.048C10.64 6.432 11.096 7.016 11.352 7.8C11.992 6.248 13.168 5.472 14.88 5.472C15.856 5.472 16.72 5.776 17.472 6.384C18.224 6.992 18.6 7.936 18.6 9.216V14.184H20.04V16.512Z" fill="currentColor"></path><path d="M33.6714 16.512H29.1354V14.496C28.8314 15.12 28.3834 15.656 27.7914 16.104C27.1994 16.536 26.4154 16.752 25.4394 16.752C24.0154 16.752 22.8954 16.264 22.0794 15.288C21.2634 14.312 20.8554 12.984 20.8554 11.304C20.8554 9.688 21.2554 8.312 22.0554 7.176C22.8554 6.04 24.0634 5.472 25.6794 5.472C26.5594 5.472 27.2794 5.648 27.8394 6C28.3994 6.352 28.8314 6.8 29.1354 7.344V2.352H26.9754V0H32.2314V14.184H33.6714V16.512ZM29.1354 11.04V10.776C29.1354 9.88 28.8954 9.184 28.4154 8.688C27.9514 8.176 27.3674 7.92 26.6634 7.92C25.9754 7.92 25.3674 8.176 24.8394 8.688C24.3274 9.2 24.0714 10.008 24.0714 11.112C24.0714 12.152 24.3114 12.944 24.7914 13.488C25.2714 14.032 25.8394 14.304 26.4954 14.304C27.3114 14.304 27.9514 13.96 28.4154 13.272C28.8954 12.584 29.1354 11.84 29.1354 11.04Z" fill="currentColor"></path><path d="M47.9589 16.512H41.9829V14.184H43.4229V10.416C43.4229 9.488 43.2629 8.824 42.9429 8.424C42.6389 8.024 42.1829 7.824 41.5749 7.824C40.8389 7.824 40.2709 8.056 39.8709 8.52C39.4709 8.968 39.2629 9.56 39.2469 10.296V14.184H40.6869V16.512H34.7109V14.184H36.1509V8.04H34.5909V5.688H39.2469V7.344C39.9669 6.096 41.1269 5.472 42.7269 5.472C43.7509 5.472 44.6389 5.776 45.3909 6.384C46.1429 6.992 46.5189 7.936 46.5189 9.216V14.184H47.9589V16.512Z" fill="currentColor"></path></svg></a><p>Your blueprint for a better internet.</p><ul class="social-icons"><li><a href="https://mozilla.social/@mdn" target="_blank" rel="me noopener noreferrer"><span class="icon icon-mastodon"></span><span class="visually-hidden">MDN on Mastodon</span></a></li><li><a href="https://twitter.com/mozdevnet" target="_blank" rel="noopener noreferrer"><span class="icon icon-twitter-x"></span><span class="visually-hidden">MDN on X (formerly Twitter)</span></a></li><li><a href="https://github.com/mdn/" target="_blank" rel="noopener noreferrer"><span class="icon icon-github-mark-small"></span><span class="visually-hidden">MDN on GitHub</span></a></li><li><a href="/en-US/blog/rss.xml" target="_blank"><span class="icon icon-feed"></span><span class="visually-hidden">MDN Blog RSS Feed</span></a></li></ul></div><div class="page-footer-nav-col-1"><h2 class="footer-nav-heading">MDN</h2><ul class="footer-nav-list"><li class="footer-nav-item"><a href="/en-US/about">About</a></li><li class="footer-nav-item"><a href="/en-US/blog/">Blog</a></li><li class="footer-nav-item"><a href="https://www.mozilla.org/en-US/careers/listings/?team=ProdOps" target="_blank" rel="noopener noreferrer">Careers</a></li><li class="footer-nav-item"><a href="/en-US/advertising">Advertise with us</a></li></ul></div><div class="page-footer-nav-col-2"><h2 class="footer-nav-heading">Support</h2><ul class="footer-nav-list"><li class="footer-nav-item"><a class="footer-nav-link" href="https://support.mozilla.org/products/mdn-plus">Product help</a></li><li class="footer-nav-item"><a class="footer-nav-link" href="/zh-CN/docs/MDN/Community/Issues">Report an issue</a></li></ul></div><div class="page-footer-nav-col-3"><h2 class="footer-nav-heading">Our communities</h2><ul class="footer-nav-list"><li class="footer-nav-item"><a class="footer-nav-link" href="/en-US/community">MDN Community</a></li><li class="footer-nav-item"><a class="footer-nav-link" href="https://discourse.mozilla.org/c/mdn/236" target="_blank" rel="noopener noreferrer">MDN Forum</a></li><li class="footer-nav-item"><a class="footer-nav-link" href="/discord" target="_blank" rel="noopener noreferrer">MDN Chat</a></li></ul></div><div class="page-footer-nav-col-4"><h2 class="footer-nav-heading">Developers</h2><ul class="footer-nav-list"><li class="footer-nav-item"><a class="footer-nav-link" href="/zh-CN/docs/Web">Web Technologies</a></li><li class="footer-nav-item"><a class="footer-nav-link" href="/zh-CN/docs/Learn">Learn Web Development</a></li><li class="footer-nav-item"><a class="footer-nav-link" href="/zh-CN/plus">MDN Plus</a></li><li class="footer-nav-item"><a href="https://hacks.mozilla.org/" target="_blank" rel="noopener noreferrer">Hacks Blog</a></li></ul></div><div class="page-footer-moz"><a href="https://www.mozilla.org/" class="footer-moz-logo-link" target="_blank" rel="noopener noreferrer"><svg width="112" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"><title id="mozilla-footer-logo-svg">Mozilla logo</title><path d="M41.753 14.218c-2.048 0-3.324 1.522-3.324 4.157 0 2.423 1.119 4.286 3.29 4.286 2.082 0 3.447-1.678 3.447-4.347 0-2.826-1.522-4.096-3.413-4.096Zm54.89 7.044c0 .901.437 1.618 1.645 1.618 1.427 0 2.949-1.024 3.044-3.352-.649-.095-1.365-.185-2.02-.185-1.426-.005-2.668.397-2.668 1.92Z" fill="currentColor"></path><path d="M0 0v32h111.908V0H0Zm32.56 25.426h-5.87v-7.884c0-2.423-.806-3.352-2.39-3.352-1.924 0-2.702 1.365-2.702 3.324v4.868h1.864v3.044h-5.864v-7.884c0-2.423-.806-3.352-2.39-3.352-1.924 0-2.702 1.365-2.702 3.324v4.868h2.669v3.044H6.642v-3.044h1.863v-7.918H6.642V11.42h5.864v2.11c.839-1.489 2.3-2.39 4.252-2.39 2.02 0 3.878.963 4.566 3.01.778-1.862 2.361-3.01 4.566-3.01 2.512 0 4.812 1.522 4.812 4.84v6.402h1.863v3.044h-.005Zm9.036.307c-4.314 0-7.296-2.635-7.296-7.106 0-4.096 2.484-7.481 7.514-7.481s7.481 3.38 7.481 7.29c0 4.472-3.228 7.297-7.699 7.297Zm22.578-.307H51.942l-.403-2.11 7.7-8.846h-4.376l-.621 2.17-2.888-.313.498-4.907h12.294l.313 2.11-7.767 8.852h4.533l.654-2.172 3.167.308-.872 4.908Zm7.99 0h-4.191v-5.03h4.19v5.03Zm0-8.976h-4.191v-5.03h4.19v5.03Zm2.618 8.976 6.054-21.358h3.945l-6.054 21.358h-3.945Zm8.136 0 6.048-21.358h3.945l-6.054 21.358h-3.939Zm21.486.307c-1.863 0-2.887-1.085-3.072-2.792-.805 1.427-2.232 2.792-4.498 2.792-2.02 0-4.314-1.085-4.314-4.006 0-3.447 3.323-4.253 6.518-4.253.778 0 1.584.034 2.3.124v-.465c0-1.427-.034-3.133-2.3-3.133-.84 0-1.488.061-2.143.402l-.453 1.578-3.195-.34.549-3.224c2.45-.996 3.692-1.27 5.992-1.27 3.01 0 5.556 1.55 5.556 4.75v6.083c0 .805.314 1.085.963 1.085.184 0 .375-.034.587-.095l.034 2.11a5.432 5.432 0 0 1-2.524.654Z" fill="currentColor"></path></svg></a><ul class="footer-moz-list"><li class="footer-moz-item"><a href="https://www.mozilla.org/privacy/websites/" class="footer-moz-link" target="_blank" rel="noopener noreferrer">Website Privacy Notice</a></li><li class="footer-moz-item"><a href="https://www.mozilla.org/privacy/websites/#cookies" class="footer-moz-link" target="_blank" rel="noopener noreferrer">Cookies</a></li><li class="footer-moz-item"><a href="https://www.mozilla.org/about/legal/terms/mozilla" class="footer-moz-link" target="_blank" rel="noopener noreferrer">Legal</a></li><li class="footer-moz-item"><a href="https://www.mozilla.org/about/governance/policies/participation/" class="footer-moz-link" target="_blank" rel="noopener noreferrer">Community Participation Guidelines</a></li></ul></div><div class="page-footer-legal"><p id="license" class="page-footer-legal-text">Visit<!-- --> <a href="https://www.mozilla.org" target="_blank" rel="noopener noreferrer">Mozilla Corporation’s</a> <!-- -->not-for-profit parent, the<!-- --> <a target="_blank" rel="noopener noreferrer" href="https://foundation.mozilla.org/">Mozilla Foundation</a>.<br/>Portions of this content are ©1998–<!-- -->2024<!-- --> by individual mozilla.org contributors. Content available under<!-- --> <a href="/zh-CN/docs/MDN/Writing_guidelines/Attrib_copyright_license">a Creative Commons license</a>.</p></div></div></footer></div><script type="application/json" id="hydration">{"url":"/zh-CN/docs/Games/Anatomy","doc":{"isMarkdown":true,"isTranslated":true,"isActive":true,"flaws":{},"title":"剖析游戏结构","mdn_url":"/zh-CN/docs/Games/Anatomy","locale":"zh-CN","native":"中文 (简体)","sidebarHTML":"\n <ol>\n <li class=\"toggle\">\n <details open=\"\">\n <summary>简介</summary>\n <ol>\n <li><a href=\"/zh-CN/docs/Games/Introduction\">简介</a></li>\n <li><em><a href=\"/zh-CN/docs/Games/Anatomy\" aria-current=\"page\">剖析</a></em></li>\n </ol>\n </details>\n </li>\n <li class=\"toggle\">\n <details>\n <summary>游戏开发用到的 API</summary>\n <ol>\n <li><a href=\"/zh-CN/docs/Web/API/Canvas_API\">Canvas</a></li>\n <li><a href=\"/zh-CN/docs/Web/CSS\">CSS</a></li>\n <li><a href=\"/zh-CN/docs/Web/API/Fullscreen_API\">全屏</a></li>\n <li><a href=\"/zh-CN/docs/Web/API/Gamepad_API\">游戏控制器</a></li>\n <li><a href=\"/zh-CN/docs/Web/API/IndexedDB_API\">IndexedDB</a></li>\n <li><a href=\"/zh-CN/docs/Web/JavaScript\">JavaScript</a></li>\n <li><a href=\"/zh-CN/docs/Web/API/Pointer_Lock_API\">指针锁定</a></li>\n <li><a href=\"/zh-CN/docs/Web/SVG\">SVG</a></li>\n <li><a href=\"/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray\">类型数组</a></li>\n <li><a href=\"/zh-CN/docs/Web/API/Web_Audio_API\">Web 音频</a></li>\n <li><a href=\"/zh-CN/docs/Web/API/WebGL_API\">WebGL</a></li>\n <li><a href=\"/zh-CN/docs/Web/API/WebRTC_API\">WebRTC</a></li>\n <li><a href=\"/zh-CN/docs/Web/API/WebSockets_API\">WebSocket</a></li>\n <li><a href=\"/zh-CN/docs/Web/API/WebVR_API\">WebVR</a></li>\n <li><a href=\"/zh-CN/docs/Web/API/Web_Workers_API\">Web Worker</a></li>\n <li><a href=\"/zh-CN/docs/Web/API/XMLHttpRequest\">XMLHttpRequest</a></li>\n </ol>\n </details>\n </li>\n <li class=\"toggle\">\n <details>\n <summary>技术</summary>\n <ol>\n <li><a href=\"/zh-CN/docs/Games/Techniques/Async_scripts\">使用 asm.js 中的异步脚本</a></li>\n <li><a href=\"/zh-CN/docs/Web/Performance/Optimizing_startup_performance\">优化启动性能</a></li>\n <li><a class=\"only-in-en-us\" title=\"此页面目前仅提供英文版本\" href=\"/en-US/docs/Games/Techniques/WebRTC_data_channels\">使用 WebRTC 点对点数据通道</a></li>\n <li><a class=\"only-in-en-us\" title=\"此页面目前仅提供英文版本\" href=\"/en-US/docs/Games/Techniques/Audio_for_Web_Games\">Web 游戏的音频</a></li>\n <li><a href=\"/zh-CN/docs/Games/Techniques/2D_collision_detection\">2D 碰撞检测</a></li>\n <li><a class=\"only-in-en-us\" title=\"此页面目前仅提供英文版本\" href=\"/en-US/docs/Games/Techniques/Tilemaps\">平铺和平铺贴图概述</a></li>\n </ol>\n </details>\n </li>\n <li class=\"toggle\">\n <details>\n <summary>Web 中的 3D 游戏</summary>\n <ol>\n <li><a href=\"/zh-CN/docs/Games/Techniques/3D_on_the_web\">概述</a></li>\n <li><a href=\"/zh-CN/docs/Games/Techniques/3D_on_the_web/Basic_theory\">基本的 3D 理论</a></li>\n <li><a class=\"only-in-en-us\" title=\"此页面目前仅提供英文版本\" href=\"/en-US/docs/Games/Techniques/3D_on_the_web/Building_up_a_basic_demo_with_A-Frame\">使用 A-Frame 构建基础演示</a></li>\n <li><a class=\"only-in-en-us\" title=\"此页面目前仅提供英文版本\" href=\"/en-US/docs/Games/Techniques/3D_on_the_web/Building_up_a_basic_demo_with_Babylon.js\">使用 Babylon.js 构建基础演示</a></li>\n <li><a class=\"only-in-en-us\" title=\"此页面目前仅提供英文版本\" href=\"/en-US/docs/Games/Techniques/3D_on_the_web/Building_up_a_basic_demo_with_PlayCanvas\">使用 PlayCanvas 构建基础演示</a></li>\n <li><a href=\"/zh-CN/docs/Games/Techniques/3D_on_the_web/Building_up_a_basic_demo_with_Three.js\">使用 Three.js 构建基础演示</a></li>\n <li><a class=\"only-in-en-us\" title=\"此页面目前仅提供英文版本\" href=\"/en-US/docs/Games/Techniques/3D_on_the_web/WebXR\">WebXR</a></li>\n <li><a href=\"/zh-CN/docs/Games/Techniques/3D_collision_detection\">3D 碰撞检测</a></li>\n <li><a class=\"only-in-en-us\" title=\"此页面目前仅提供英文版本\" href=\"/en-US/docs/Games/Techniques/3D_collision_detection/Bounding_volume_collision_detection_with_THREE.js\">使用 THREE.js 进行边界体积碰撞检测</a></li>\n </ol>\n </details>\n </li>\n <li class=\"toggle\">\n <details>\n <summary>实现游戏控制机制</summary>\n <ol>\n <li><a href=\"/zh-CN/docs/Games/Techniques/Control_mechanisms\">控制机制</a></li>\n <li><a href=\"/zh-CN/docs/Games/Techniques/Control_mechanisms/Mobile_touch\">移动端触摸</a></li>\n <li><a class=\"only-in-en-us\" title=\"此页面目前仅提供英文版本\" href=\"/en-US/docs/Games/Techniques/Control_mechanisms/Desktop_with_mouse_and_keyboard\">通过鼠标和键盘控制的桌面端</a></li>\n <li><a class=\"only-in-en-us\" title=\"此页面目前仅提供英文版本\" href=\"/en-US/docs/Games/Techniques/Control_mechanisms/Desktop_with_gamepad\">通过游戏手柄控制的桌面端</a></li>\n <li><a class=\"only-in-en-us\" title=\"此页面目前仅提供英文版本\" href=\"/en-US/docs/Games/Techniques/Control_mechanisms/Other\">其他</a></li>\n </ol>\n </details>\n </li>\n <li class=\"toggle\">\n <details>\n <summary>教程</summary>\n <ol>\n <li><a href=\"/zh-CN/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript\">使用纯 JavaScript 实现 2D 打砖块游戏</a></li>\n <li><a href=\"/zh-CN/docs/Games/Tutorials/2D_breakout_game_Phaser\">使用 Phaser 实现 2D 打砖块游戏</a></li>\n <li><a class=\"only-in-en-us\" title=\"此页面目前仅提供英文版本\" href=\"/en-US/docs/Games/Tutorials/HTML5_Gamedev_Phaser_Device_Orientation\">具有设备方向检测功能的 2D 迷宫游戏</a></li>\n <li><a href=\"https://mozdevs.github.io/html5-games-workshop/en/guides/platformer/start-here/\">使用 Phaser 实现 2D 平板游戏</a></li>\n </ol>\n </details>\n </li>\n <li class=\"toggle\">\n <details>\n <summary>发布游戏</summary>\n <ol>\n <li><a href=\"/zh-CN/docs/Games/Publishing_games\">概述</a></li>\n <li><a href=\"/zh-CN/docs/Games/Publishing_games/Game_distribution\">游戏分发</a></li>\n <li><a href=\"/zh-CN/docs/Games/Publishing_games/Game_promotion\">游戏推广</a></li>\n <li><a href=\"/zh-CN/docs/Games/Publishing_games/Game_monetization\">游戏盈利</a></li>\n </ol>\n </details>\n </li>\n </ol>\n","sidebarMacro":"GamesSidebar","body":[{"type":"prose","value":{"id":null,"title":null,"isH3":false,"content":"<p>本文从技术角度分析了一般电子游戏的结构和工作流程,就此介绍主循环是如何运行的。它有助于初学者了解在现代游戏开发领域构建游戏时需要什么,以及理解像 JavaScript 这样的 Web 标准是如何成为可用于开发游戏的工具的。游戏开发经验丰富但不熟悉 Web 开发的开发者也能从本文中受益。</p>"}},{"type":"prose","value":{"id":"呈现、接收、解释、计算、重复","title":"呈现、接收、解释、计算、重复","isH3":false,"content":"<p>所有电子游戏的目标都是向用户<strong>呈现</strong>一个场景,<strong>接收</strong>他们的输入,将这些输入信号<strong>解释</strong>为动作,并<strong>计算</strong>出由这些动作产生的新场景。游戏不断地循环遍历,一遍又一遍,直到遇到某些终止条件(比如赢、输或者用户退出休息)。毫不奇怪,这种模式与游戏引擎的编程方式相呼应。</p>\n<p>具体情况取决于游戏本身。</p>\n<p>一些游戏通过<strong>用户输入</strong>来驱动这个循环。想象一下,你正在开发一种“<em>找不同</em>”类型的游戏。这些游戏向用户<strong>呈现</strong>两张图片、<strong>接收</strong>用户的点击(或触摸)、将用户输入<strong>解释</strong>为成功、失败、暂停、菜单交互等。最后,游戏根据用户的输入<strong>计算</strong>得到新的游戏场景。这种游戏是由用户的输入驱动,也就是说,它会在用户进行输入之后冻结画面,等待玩家进行新的输入。这是一种基于回合的游戏类型,它不需要每帧持续更新画面,只有当玩家作出反应时才会。</p>\n<p>另一些游戏需要尽可能控制每一个细微的时间片段。与上述原理有点小区别:每个动画帧都将循环遍历,并在之后第一个可用的轮次捕获玩家输入的任何变化。这种每帧一次的模型是通过一个叫<strong>主循环</strong>的东西实现的。如果你的游戏循环是基于时间的,则必须保证对主循环精准的模拟。</p>\n<p>但它也可能不需要逐帧控制。你的游戏循环可能类似于<em>找不同</em>的例子,并且以输入事件作为基础,那么它可能同时需要输入和模拟时间片段。它甚至可以基于其他的东西来循环。</p>\n<p>正如我们将再下一节中所描述的,现代 JavaScript 可以轻松开发出一个高效的,逐帧执行的主循环。当然,你的游戏只会按照你所做的那样优化。如果某些东西看起来应该被添加到一个更少发生的事件里,那么将它从主循环中剥离出来通常是个好主意(但并非总是如此)。</p>"}},{"type":"prose","value":{"id":"在_javascript_中构建一个主循环","title":"在 JavaScript 中构建一个主循环","isH3":false,"content":"<p>JavaScript 能很好的处理事件和回调函数。现代浏览器努力在需要的时候调用方法,并在间隙中闲下来(或做其他任务)。将你的代码附加到适合它们的时刻是一个很好的主意。考虑一下你的函数是需要在严格的时间周期内,还是每一帧,或者仅仅是在发生了其他情况之后执行。当你的函数需要被调用时,要更具体地使用浏览器,这样浏览器就可以在调用时进行优化。而且,这可能会让你的工作更轻松。</p>\n<p>有些代码需要逐帧运行,所以应将其附加到浏览器的重绘周期中,没有比这更好的了!在 Web 中通常将 <a href=\"/zh-CN/docs/Web/API/Window/requestAnimationFrame\"><code>window.requestAnimationFrame()</code></a> 方法作为大多数良好的逐帧循环的基础。在调用该方法时必须传入一个回调函数,这个回调函数将在下一次重新绘制之前执行。下面是一个简单的主循环的例子:</p>\n<div class=\"code-example\"><div class=\"example-header\"><span class=\"language-name\">js</span></div><pre class=\"brush: js notranslate\"><code>window.main = () => {\n window.requestAnimationFrame(main);\n\n // 你的主循环可以做你想要做的任何事情\n};\n\nmain(); // 开始主循环\n</code></pre></div>\n<div class=\"notecard note\">\n <p><strong>备注:</strong>在这里讨论的每个 <code>main()</code> 方法中,在执行循环内容之前,我们会递归调用一个新的 <code>requestAnimationFrame</code>。这不是毫无根据而写的:这样做被认为是最佳实践。如果你的当前帧错过了它的垂直同步窗口的周期,你也在下一个帧通过 <code>requestAnimationFrame</code> 尽早的调用 <code>main()</code>,从而确保浏览器能够及时地执行。</p>\n</div>\n<p>上面的代码块有两个语句。第一个语句创建一个名为 <code>main()</code> 的全局变量的函数。这个函数会执行一些操作,同时告诉浏览器在下一帧通过 <code>window.requestAnimationFrame()</code> 调用本身。第二个语句调用第一个语句中定义的 <code>main()</code> 函数。因为 <code>main()</code> 中在第二个语句中被调用一次,而每次调用都将自身又放置到下一个帧的执行队列中,因此 <code>main()</code> 的调用与你的帧率同步。</p>\n<p>当然,这个循环并不完美。不过在讨论如何改善之前,让我们先讨论一下它已经做到了什么。</p>\n<p>将主循环的时机安排在浏览器每次的绘制显示中,允许你能像浏览器想要绘制的那样频繁的运行你的循环。你能够控制每一帧动画,并且 <code>main()</code> 是循环中唯一执行的函数,所以这很简单。主视角射击游戏(或类似的游戏)每一帧都会出现一个新的场景。没有比这种方法更平滑并绘制及时的了。</p>\n<p>但是不要马上认为动画必需要帧帧控制。通过 CSS 动画或浏览器中的其他工具,我们也可以很容易实现简单的动画,甚至能用上 GPU 加速。还有有很多类似这样的工具能让我们的开发变得更加简单。</p>"}},{"type":"prose","value":{"id":"在_javascript_中构建一个更好的主循环","title":"在 JavaScript 中构建一个更好的主循环","isH3":false,"content":"<p>在前面的主循环中有两个明显的问题:<code>main()</code> 污染了 <a href=\"/zh-CN/docs/Web/API/Window\"><code>window</code></a> 对象(所有全局变量存储的对象),并且示例代码没有给我们留下一个<em>停止</em>循环的方法(除非整个标签页被关闭或刷新)。对于第一个问题,如果你希望主循环只运行,并且不需要被简单(直接)地访问,你可以将它创建为一个立即调用的函数表达式(IIFE)。</p>\u003c!-- prettier-ignore-start -->\n<div class=\"code-example\"><div class=\"example-header\"><span class=\"language-name\">js</span></div><pre class=\"brush: js notranslate\"><code>/*\n* 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入(ASI)。 \n* 浏览器可能会意外地认为整个示例会从上一行继续。\n* 如果前一行不为空或终止,则前面的分号标志着新行的开始。\n*/\n\n;(() => {\n function main(){\n window.requestAnimationFrame(main);\n\n // 你的主循环内容。\n }\n\n main(); // 开始循环\n})();\n</code></pre></div>\u003c!-- prettier-ignore-end -->\n<p>当浏览器遇到这个 IIFE 时,它将定义你的主循环,并立即将其加入下一个帧的更新队列中。<code>main</code>(或作为方法而言的 <code>main()</code>)不会被附加到任何对象。它在应用程序的其他地方仍是一个有效的未使用的名称,仍可以自由地被定义为其他的东西。</p>\n<div class=\"notecard note\">\n <p><strong>备注:</strong>在实践中,更常见的终止下一个 <code>requestAnimationFrame()</code> 方式是使用 if 语句,而不是调用 <code>cancelAnimationFrame()</code>。</p>\n</div>\n<p>对于第二个问题,要终止循环,你需要调用 <a href=\"/zh-CN/docs/Web/API/Window/cancelAnimationFrame\"><code>window.cancelAnimationFrame()</code></a> 来取消 <code>main()</code> 的调用。该方法需要传入你最后一次调用 <code>requestAnimationFrame()</code> 时返回的 ID。让我们假设你的游戏的函数和变量是建立在你称为 <code>MyGame</code> 的名称空间上。扩展我们的最后一个例子,主循环现在看起来是这样的:</p>\u003c!-- prettier-ignore-start -->\n<div class=\"code-example\"><div class=\"example-header\"><span class=\"language-name\">js</span></div><pre class=\"brush: js notranslate\"><code>/*\n* 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入(ASI)。 \n* 浏览器可能会意外地认为整个示例会从上一行继续。\n* 如果前一行不为空或终止,则前面的分号标志着新行的开始。\n*\n* 让我们假设 MyGame 是之前定义的。\n*/\n\n;(() => {\n function main(){\n MyGame.stopMain = window.requestAnimationFrame(main);\n\n // 你的主循环内容\n }\n\n main(); // 开始循环\n})();\n</code></pre></div>\u003c!-- prettier-ignore-end -->\n<p>现在,我们在 <code>MyGame</code> 名称空间中声明了一个变量 <code>stopMain</code>,其值为主循环最后调用 <code>requestAnimationFrame()</code> 时返回的 ID。任何时候,我们可以通过告诉浏览器取消与 ID 相对应的请求来停止主循环。</p>\n<div class=\"code-example\"><div class=\"example-header\"><span class=\"language-name\">js</span></div><pre class=\"brush: js notranslate\"><code>window.cancelAnimationFrame(MyGame.stopMain);\n</code></pre></div>\n<p>在 JavaScript 的中编写主循环的关键在于考虑到任何会驱动你行为的事件,同时要注意不同的系统是如何相互作用的。你可能拥有多个由多个不同类型的事件驱动的组件。这看起来像是不必要地变复杂了,但它可能也是一个很好的优化(当然也不一定)。问题是,你并不是在编写一个典型的主循环。在 JavaScript 中,你使用的是浏览器的主循环,并且你正在尝试这样做。</p>"}},{"type":"prose","value":{"id":"用_javascript_构建一个更优化的主循环","title":"用 JavaScript 构建一个更优化的主循环","isH3":false,"content":"<p>基本上,在 JavaScript 的中,浏览器有它自己的主循环,而你的代码存在于循环某些阶段。上面描述的主循环,试图避免脱离浏览器的控制。这种主循环附着于 <code>window.requestAnimationFrame()</code> 方法,该方法将在浏览器的下一帧中执行,具体取决于浏览器如何与将其自己的主循环关联起来。<a href=\"https://www.w3.org/TR/animation-timing/\" class=\"external\" target=\"_blank\">W3C 的 requestAnimationFrame 规范</a>并没有真正定义什么时候浏览器必须执行 requestAnimationFrame 回调。这有一个好处,浏览器厂商可以自由地实现他们认为最好的解决方案,并随着时间的推移进行调整。</p>\n<p>现代版的 Firefox 和 Google Chrome(可能还有其他浏览器)都尝试图在框架的时间片段的开始时<em>尝试</em>将 <code>requestAnimationFrame</code> 回调与它们的主线程进行连接。因此,浏览器的主线程<em>看起来</em>就像下面这样:</p>\n<ol>\n <li>启动一个新帧(而之前的帧由显示器处理)。</li>\n <li>遍历 <code>requestAnimationFrame</code> 回调列表并调用它们。</li>\n <li>当上面的回调停止控制主线程时,执行垃圾收集和其他帧任务。</li>\n <li>睡眠(除非事件打断了浏览器的睡眠),直到显示器准备好你的图像(<a href=\"https://www.techopedia.com/definition/92/vertical-sync-vsync\" class=\"external\" target=\"_blank\">垂直同步</a>)并重复。</li>\n</ol>\n<p>你可以考虑开发实时应用程序,因为有时间做工作。所有上述步骤都必须在每 16 毫秒内进行一次,以跟上 60Hz 的显示器。浏览器会尽可能早地调用你的代码,从而给它最大的计算时间。你的主线程通常会启动一些甚至不在主线程上的工作负载(如 WebGL 的中的光栅化或着色器)。在浏览器使用其主线程管理垃圾收集,其他任务或处理异步事件时,可以在 Web Worker 或 GPU 上执行长时间的计算。</p>\n<p>当我们讨论预算时间时,许多 Web 浏览器都有一个称为<em>高解析度时间</em>的工具。<a href=\"/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date\"><code>Date</code></a> 对象不再是公认的事件计时方法,因为它非常不精确,可以由系统时钟进行修改。另一方面,高解析度时间计算自 <code>navigationStart</code>(当上一个文档被卸载时)的毫秒数。这个值以小数的精度返回,精确到千分之一毫秒。它被称为 <a href=\"/zh-CN/docs/Web/API/DOMHighResTimeStamp\"><code>DOMHighResTimeStamp</code></a>,但是,无论出于什么意图和目的,都认为它是一个浮点数。</p>\n<div class=\"notecard note\">\n <p><strong>备注:</strong>系统(硬件或软件)不能达到微秒精度,则至少提供毫秒级的精度。不过,如果能够达到微秒的精度,那就应该提供这一精度。</p>\n</div>\n<p>这个值本身并不太有用,因为它与一个相当无趣的事件相关,但它可以从另一个时间戳中减去,以便准确准确地确定这两个点之间的时间间隔。要获得这些时间戳中的一个,你可以调用 <code>window.performance.now()</code> 并将结果存储为一个变量。</p>\n<div class=\"code-example\"><div class=\"example-header\"><span class=\"language-name\">js</span></div><pre class=\"brush: js notranslate\"><code>const tNow = window.performance.now();\n</code></pre></div>\n<p>回到主循环的主题。你将经常想知道何时调用主函数。因为这是常见的,<code>window.requestAnimationFrame()</code> 总是在执行回调函数时提供 <code>DOMHighResTimeStamp</code> 作为其参数。这将为我们之前的主循环带来另一个增强。</p>\u003c!-- prettier-ignore-start -->\n<div class=\"code-example\"><div class=\"example-header\"><span class=\"language-name\">js</span></div><pre class=\"brush: js notranslate\"><code>/*\n* 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入(ASI)。 \n* 浏览器可能会意外地认为整个示例会从上一行继续。\n* 如果前一行不为空或终止,则前面的分号标志着新行的开始。\n*\n* 我们还假设 MyGame 是以前定义的。\n*/\n\n;(() => {\n function main(tFrame){\n MyGame.stopMain = window.requestAnimationFrame(main);\n\n // 你的主循环内容\n // tFrame,来源于“function main(tFrame)”,现在变为是由 rAF 提供的 DOMHighResTimeStamp。\n }\n\n main(); // 开始循环\n})();\n</code></pre></div>\u003c!-- prettier-ignore-end -->\n<p>其他一些优化是可能的,这取决于你的游戏想要完成什么。你的游戏类型显然会有所不同,但它甚至可能比这更微妙。你可以在画布上单独绘制每个像素,也可以将 DOM 元素(包括具有透明背景的多个 WebGL 的画布)放入复杂的层次结构中。每条路径都将带来不同的机会和约束。</p>"}},{"type":"prose","value":{"id":"决定……时间","title":"决定……时间","isH3":false,"content":"<p>你将需要对你的主循环作出艰难的决定:如何模拟准确的时间进度。如果你想要控制每一帧,那么你需要确定你的游戏更新和绘制的频率,你甚至可能希望以不同的速率进行更新和绘制。你还需要考虑如果用户的系统无法跟上工作负载,那么你还需考虑如何优雅降级,以保证性能。让我们首先假定你会在每次绘制时,同时处理用户的输入,并更新游戏状态。我们稍后再细讲。</p>\n<div class=\"notecard note\">\n <p><strong>备注:</strong>改变主循环如何处理时间是非常困难的,在进行主循环之前,请仔细考虑你的需求。</p>\n</div>"}},{"type":"prose","value":{"id":"大多数浏览器游戏应该是什么样的","title":"大多数浏览器游戏应该是什么样的","isH3":true,"content":"<p>如果你的游戏可以达到你所支持的任何硬件的最大刷新率,那么你的工作就变得相当容易了。你可以简单地进行更新、渲染,然后在垂直同步之前什么都不用做。</p>\u003c!-- prettier-ignore-start -->\n<div class=\"code-example\"><div class=\"example-header\"><span class=\"language-name\">js</span></div><pre class=\"brush: js notranslate\"><code>/*\n* 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入(ASI)。 \n* 浏览器可能会意外地认为整个示例从上一行继续。\n* 如果前一行不为空或终止,则前面的分号标志着新行的开始。\n*\n* 我们还假设 MyGame 是以前定义的。\n*/\n\n;(() => {\n function main(tFrame) {\n MyGame.stopMain = window.requestAnimationFrame(main);\n\n update(tFrame); // 调用 update 方法。在我们的例子中,我们给它 rAF 的时间戳。\n render();\n }\n\n main(); // 开始循环\n})();\n</code></pre></div>\u003c!-- prettier-ignore-end -->\n<p>如果无法达到最大刷新率,可以调整画面质量设置以保持你的时间预算。这个概念最有名的例子是 id Software 的游戏——狂怒。这个游戏取消了用户的控制权,以使其计算时间保持在大约 16ms(或大约 60fps)。如果计算时间过长,渲染的分辨率就会降低,纹理和其他资源将无法加载或绘制等。这个(非 Web 的)案例研究做了一些假设和折衷:</p>\n<ul>\n <li>每个动画帧都考虑用户输入。</li>\n <li>没有帧需要外推(猜测),因为每个绘图都有自己的更新。</li>\n <li>仿真系统基本上可以假定每次完全更新间隔约 16ms。</li>\n <li>给用户控制质量设置将是一场噩梦。</li>\n <li>不同的监视器以不同的速率输入:30FPS、75FPS、100FPS、120FPS、144FPS 等</li>\n <li>无法跟上 60 FPS 的系统会失去视觉质量,以保持游戏运行的最佳速度(最终如果质量太低,则会是彻底的失败)。</li>\n</ul>"}},{"type":"prose","value":{"id":"处理可变刷新率需求的其他方法","title":"处理可变刷新率需求的其他方法","isH3":true,"content":"<p>存在其他解决问题的方法。</p>\n<p>一种常见的技术是以恒定的频率更新模拟,然后绘制尽可能多的(或尽可能少的)实际帧。更新方法可以继续循环,而不用考虑用户看到的内容。绘图方法可以查看最后的更新以及发生的时间。由于绘制知道何时表示,以及上次更新的模拟时间,它可以预测为用户绘制一个合理的框架。这是否比官方更新循环更频繁(甚至更不频繁)无关紧要。更新方法设置检查点,并且像系统允许的那样频繁地,渲染方法画出周围的时间。在 Web 标准中分离更新有很多种方法:</p>\n<ul>\n <li>\n <p>在 <code>requestAnimationFrame()</code> 中绘制,并在 <a href=\"/zh-CN/docs/Web/API/Window/setInterval\" title=\"setInterval()\"><code>setInterval()</code></a> 或 <a href=\"/zh-CN/docs/Web/API/Window/setTimeout\" title=\"setTimeout()\"><code>setTimeout()</code></a> 中更新。</p>\n <ul>\n <li>即使在未聚焦或最小化的情况下,使用处理器时间,也可能是主线程,并且可能是传统游戏循环的工件(但是很简单)。</li>\n </ul>\n </li>\n <li>\n <p>在 <code>requestAnimationFrame()</code> 中绘制,并在 <a href=\"/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers\">Web Worker</a> 的 <a href=\"/zh-CN/docs/Web/API/WorkerGlobalScope/setInterval\" title=\"setInterval()\"><code>setInterval()</code></a> 或 <a href=\"/zh-CN/docs/Web/API/WorkerGlobalScope/setTimeout\" title=\"setTimeout()\"><code>setTimeout()</code></a> 中对其进行更新。</p>\n <ul>\n <li>这与上述相同,除了更新不会使主线程(主线程也没有)。这是一个更复杂的解决方案,并且对于简单更新可能会有太多的开销。</li>\n </ul>\n </li>\n <li>\n <p>在 <code>requestAnimationFrame()</code> 中绘制,并使用它来戳一个包含更新方法的 Web Worker,其中包含要计算的刻度数(如果有的话)。</p>\n <ul>\n <li>这个睡眠直到 <code>requestAnimationFrame()</code> 被调用并且不会污染主线程,加上你不依赖于老式的方法。再次,这比以前的两个选项更复杂一些,并且<em>开始</em>每个更新将被阻止,直到浏览器决定启动 rAF 回调。</li>\n </ul>\n </li>\n</ul>\n<p>这些方法中的每一种都有类似的权衡:</p>\n<ul>\n <li>用户可以跳过渲染帧或根据其性能插入额外的帧。</li>\n <li>你可以指望所有用户以相同的固定频率更新非修饰性的变量,而不会卡顿。</li>\n <li>程序比我们前面看到的基本循环要复杂得多。</li>\n <li>用户输入完全被忽略,直到下次更新(即使用户具有快速设备)。</li>\n <li>强制插帧具有性能损失。</li>\n</ul>\n<p>单独的更新和绘图方法可能类似于下面的示例。为了演示,该示例基于第三点,只是没有使用 Web Worker 以提高可读性(老实说,也包含可写性)。</p>\n<div class=\"notecard warning\">\n <p><strong>警告:</strong>这个例子需要进行单独的技术审查。</p>\n</div>\u003c!-- prettier-ignore-start -->\n<div class=\"code-example\"><div class=\"example-header\"><span class=\"language-name\">js</span></div><pre class=\"brush: js notranslate\"><code>/*\n* 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入(ASI)。 \n* 浏览器可能会意外地认为整个示例从上一行继续。\n* 如果前一行不为空或终止,则前面的分号标志着新行的开始。\n*\n* 我们还假设 MyGame 是以前定义的。\n*\n* MyGame.lastRender 跟踪上一次提供的 requestAnimationFrame 时间戳。\n* MyGame.lastTick 跟踪上次更新时间。始终以 tickLength 递增。\n* MyGame.tickLength 是游戏状态更新的频率。这里是 20 Hz(50ms)。\n*\n* timeSinceTick 是 requestAnimationFrame 回调和上一次更新之间的时间。\n* numTicks 是这两个呈现帧之间应该发生的更新次数。\n*\n* render() 传入 tFrame, 因为 render 方法可能需要计算\n* tFrame 距离最近的更新已经过去了多久,通过外推的方式\n* 来获得场景数据。(对于快速设备,render 方法是纯装饰性的)。\n* 用以绘制场景。\n*\n* update() 根据给定时间点计算游戏状态。通常需要用 tickLength\n* 作为循环参数,递增更新。来保证游戏状态的严谨。传入 DOMHighResTimeStamp\n* 代表当前时间。(除非需要增加暂停功能,传入的时间应该总是\n* 上次更新时间 + 游戏的 tick 间隔。)\n*\n* setInitialState() 执行在运行主循环之前需要的任何任务。\n* 它只是一个你可能添加的通用示例函数。\n*/\n\n;(() => {\n function main(tFrame) {\n MyGame.stopMain = window.requestAnimationFrame(main);\n const nextTick = MyGame.lastTick + MyGame.tickLength;\n let numTicks = 0;\n\n // 如果 tFrame < nextTick,则需要更新 0 个 tick(对于 numTicks,默认为 0)。\n // 如果 tFrame = nextTick,则需要更新 1 tick(等等)。\n // 备注:正如我们在总结中提到的那样,你应该跟踪 numTicks 的大小。\n // 如果它很大,要么你的游戏是卡住了,要么机器无法跟上。\n if (tFrame > nextTick) {\n const timeSinceTick = tFrame - MyGame.lastTick;\n numTicks = Math.floor(timeSinceTick / MyGame.tickLength);\n }\n\n queueUpdates(numTicks);\n render(tFrame);\n MyGame.lastRender = tFrame;\n }\n\n function queueUpdates(numTicks) {\n for (let i = 0; i < numTicks; i++) {\n MyGame.lastTick += MyGame.tickLength; // 现在 lastTick 应是这一时间。\n update(MyGame.lastTick);\n }\n }\n\n MyGame.lastTick = performance.now();\n MyGame.lastRender = MyGame.lastTick; // 假装第一次绘制是在第一次更新。\n MyGame.tickLength = 50; // 这将使你的模拟运行在 20Hz(50ms)\n\n setInitialState();\n main(performance.now()); // 开始循环\n})();\n</code></pre></div>\u003c!-- prettier-ignore-end -->\n<p>另一个选择是简单地做一些事情不那么频繁。如果你的更新循环的一部分难以计算但对时间不敏感,则可以考虑缩小其频率,理想情况下,在延长的时间段内将其扩展成块。这是一个隐含的例子,在火炮博物馆的炮兵游戏中,他们<a href=\"https://web.archive.org/web/20161021030645/http://blog.artillery.com/2012/10/browser-garbage-collection-and-framerate.html\" class=\"external\" target=\"_blank\">调整垃圾发生率</a>来优化垃圾回收。显然,清理资源不是时间敏感的(特别是如果整理比垃圾本身更具破坏性)。</p>\n<p>这也可能适用于你自己的一些任务。那些是当可用资源成为关注点时的好候选人。</p>"}},{"type":"prose","value":{"id":"总结","title":"总结","isH3":false,"content":"<p>我知道上述的任何一种,或许没有适合你的游戏。正确的决定完全取决于你愿意(和不愿意)做出的权衡。主要关心的是切换到另一个选项。幸运的是,我没有任何经验,但我听说这是一个令人痛苦的打地鼠游戏。</p>\n<p>像 Web 这样的受管理平台,要记住的一件重要的事情是,你的循环可能会在相当长的一段时间内停止执行。当用户取消选择你的标签并且浏览器休眠(或减慢)其 <code>requestAnimationFrame</code> 回调间隔时,可能会发生这种情况。你有很多方法来处理这种情况,这可能取决于你的游戏是单人游戏还是多人游戏。一些选择是:</p>\n<ul>\n <li>\n <p>考虑差距“暂停”并跳过时间。</p>\n <ul>\n <li>你可能会看到这对大多数多人游戏来说都是有问题的。</li>\n </ul>\n </li>\n <li>\n <p>你可以模拟差距赶上。</p>\n <ul>\n <li>这可能是长时间丢失和/或复杂更新的问题。</li>\n </ul>\n </li>\n <li>\n <p>你可以从联机设备或服务器恢复游戏状态。</p>\n <ul>\n <li>除非联机设备/服务器的游戏状态已经过期,或者这是一个没有没服务器的纯单机游戏。</li>\n </ul>\n </li>\n</ul>\n<p>一旦你的主循环被开发出来,你已经决定了一套适合你的游戏的假设和权衡,现在只需要用你的决定来计算任何适用的物理、AI、声音、网络同步,以及其他任何你的游戏可能需要的。</p>"}}],"toc":[{"text":"呈现、接收、解释、计算、重复","id":"呈现、接收、解释、计算、重复"},{"text":"在 JavaScript 中构建一个主循环","id":"在_javascript_中构建一个主循环"},{"text":"在 JavaScript 中构建一个更好的主循环","id":"在_javascript_中构建一个更好的主循环"},{"text":"用 JavaScript 构建一个更优化的主循环","id":"用_javascript_构建一个更优化的主循环"},{"text":"决定……时间","id":"决定……时间"},{"text":"总结","id":"总结"}],"summary":"本文从技术角度分析了一般电子游戏的结构和工作流程,就此介绍主循环是如何运行的。它有助于初学者了解在现代游戏开发领域构建游戏时需要什么,以及理解像 JavaScript 这样的 Web 标准是如何成为可用于开发游戏的工具的。游戏开发经验丰富但不熟悉 Web 开发的开发者也能从本文中受益。","popularity":0,"modified":"2024-11-24T15:24:16.000Z","other_translations":[{"locale":"de","title":"Anatomie eines Videospiels","native":"Deutsch"},{"locale":"en-US","title":"Anatomy of a video game","native":"English (US)"},{"locale":"es","title":"Anatomía de un videojuego","native":"Español"},{"locale":"fr","title":"Anatomie d'un jeu vidéo","native":"Français"},{"locale":"ja","title":"ビデオゲームの解剖学","native":"日本語"},{"locale":"pt-BR","title":"Anatomia de um vídeo game","native":"Português (do Brasil)"},{"locale":"ru","title":"Анатомия видеоигры","native":"Русский"}],"pageType":"unknown","source":{"folder":"zh-cn/games/anatomy","github_url":"https://github.com/mdn/translated-content/blob/main/files/zh-cn/games/anatomy/index.md","last_commit_url":"https://github.com/mdn/translated-content/commit/d0d7afe1fb548a66e5ad2ed50e43afbb00114b67","filename":"index.md"},"short_title":"剖析游戏结构","parents":[{"uri":"/zh-CN/docs/Games","title":"游戏开发"},{"uri":"/zh-CN/docs/Games/Anatomy","title":"剖析游戏结构"}],"pageTitle":"剖析游戏结构 - 游戏开发 | MDN","noIndexing":false}}</script></body></html>