Files
2026-01-01 12:38:03 +00:00

188 lines
32 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html><html lang=en><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=author content="Godot Engine"><meta name=description content="GDScript is being rewritten. In this article we talk about the new tokenizer—the first step in the compilation process."><script defer data-domain=godotengine.org src=https://plausible.godot.foundation/js/script.file-downloads.outbound-links.js></script><meta property="og:site_name" content="Godot Engine"><meta property="og:url" content="https://godotengine.org/article/gdscript-progress-report-writing-tokenizer/"><meta property="og:type" content="website"><meta property="og:description" content="GDScript is being rewritten. In this article we talk about the new tokenizer—the first step in the compilation process."><meta property="og:image" content="https://godotengine.org/storage/app/uploads/public/5eb/018/a65/5eb018a659a8f843228718.png"><meta name=twitter:card content="summary_large_image"><meta property="twitter:domain" content="godotengine.org"><meta property="twitter:url" content="https://godotengine.org/article/gdscript-progress-report-writing-tokenizer/"><meta property="og:title" content="GDScript progress report: Writing a tokenizer Godot Engine"><title>GDScript progress report: Writing a tokenizer Godot Engine</title>
<link rel=alternate type=application/rss+xml title="Godot News" href=/rss.xml><link rel=alternate type=application/json title="Godot News" href=/rss.json><link rel=alternate type=application/atom+xml title="Godot News" href=/atom.xml><link rel=icon href=/assets/favicon.png sizes=any><link rel=icon href=/assets/favicon.svg type=image/svg+xml><link rel=stylesheet href=/assets/css/main.css?121><link rel=stylesheet href=/assets/css/header.css?6><link rel=stylesheet href=/assets/css/tobii.min.css><link rel=preload as=font href=/assets/fonts/Montserrat-Italic-VariableFont_wght.woff2 crossorigin><link rel=preload as=font href=/assets/fonts/Montserrat-VariableFont_wght.woff2 crossorigin><link rel=me href=https://mastodon.gamedev.place/@godotengine><input type=checkbox id=nav_toggle_cb><header class="flex column"><div class="container flex align-center"><div id=nav_head><a href=/ id=logo-link><img class=nav-logo src=/assets/logo.svg width=136 height=48 alt="Godot Engine">
<img class="nav-logo dark-logo" src=/assets/logo_dark.svg width=136 height=48 alt="Godot Engine"></a><div class=mobile-links><span class="fund mobile"><a href=https://fund.godotengine.org><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="width:13px;fill:#fff;margin-right:4px"><path d="M47.6 300.4 228.3 469.1c7.5 7 17.4 10.9 27.7 10.9s20.2-3.9 27.7-10.9L464.4 300.4c30.4-28.3 47.6-68 47.6-109.5v-5.8c0-69.9-50.5-129.5-119.4-141C347 36.5 300.6 51.4 268 84L256 96 244 84c-32.6-32.6-79-47.5-124.6-39.9C50.5 55.6.0 115.2.0 185.1v5.8c0 41.5 17.2 81.2 47.6 109.5z"/></svg> Donate</a></span>
<label for=nav_toggle_cb id=nav_toggle_btn><img src=/assets/icons/hamburger.svg width=24 height=24 alt="Main menu"></label></div></div><nav id=nav><ul class=left><li><a href=/features/ data-dropdown=features-dropdown>Features</a><li class=mobile-only><a href=/consoles/>Console support</a><li class=mobile-only><a href=/priorities/>Priorities</a><li><a href=/showcase/>Showcase</a><li><a href=/blog/>Blog</a><li><a href=/community/ data-dropdown=community-dropdown>Community</a><li><a href=https://godotengine.org/asset-library/asset>Assets</a></ul><ul class=right><li><a href=/download/windows/ class=set-os-download-url>Download</a><li><a href=https://docs.godotengine.org>Docs</a><li><a href=https://contributing.godotengine.org/en/latest/organization/how_to_contribute.html>Contribute</a><li class="fund desktop"><a href=https://fund.godotengine.org><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="width:13px;fill:#fff;margin-right:4px;top:1px;position:relative"><path d="M47.6 300.4 228.3 469.1c7.5 7 17.4 10.9 27.7 10.9s20.2-3.9 27.7-10.9L464.4 300.4c30.4-28.3 47.6-68 47.6-109.5v-5.8c0-69.9-50.5-129.5-119.4-141C347 36.5 300.6 51.4 268 84L256 96 244 84c-32.6-32.6-79-47.5-124.6-39.9C50.5 55.6.0 115.2.0 185.1v5.8c0 41.5 17.2 81.2 47.6 109.5z"/></svg> Donate</a></ul></nav></div></header><div class=nav-dropdown-menu id=features-dropdown><a href=/features/ class=touch-only>Features</a>
<a href=/consoles/>Console support</a>
<a href=/priorities/>Priorities</a></div><div class=nav-dropdown-menu id=community-dropdown><a href=/community/ class=touch-only>Community</a>
<a href=https://forum.godotengine.org>Forum</a>
<a href=/events/>Events</a></div><script>document.addEventListener("DOMContentLoaded",function(){const e=document.querySelectorAll("[data-dropdown]");e.forEach(e=>{const n=e.getAttribute("data-dropdown"),t=document.getElementById(n);if(t){let n=-1;const i=()=>{if(n===-1)return;clearTimeout(n),n=-1},a=()=>t.style.display==="block",r=()=>{if(a())return;if(window.innerWidth<1200)return;i();const n=e.getBoundingClientRect();t.style.top=n.bottom+"px",t.style.left=n.left+"px",t.style.display="block",e.classList.add("dropdown-open")},s=({instant:s=!1}={})=>{if(!a())return;const o=()=>{i(),t.style.display="none",e.classList.remove("dropdown-open")};if(s){o();return}n=setTimeout(o,100)},l=e=>{if(window.innerWidth<1200)return;e.preventDefault(),a()?s({instant:!0}):r()},c=e=>t=>{t.pointerType==="touch"&&e(t)},o=e=>t=>{t.pointerType!=="touch"&&e(t)};e.addEventListener("pointerup",c(e=>l(e))),document.documentElement.addEventListener("pointerup",c(n=>{!e.contains(n.target)&&!t.contains(n.target)&&s({instant:!0})})),e.addEventListener("pointerenter",o(e=>r())),e.addEventListener("pointerleave",o(e=>s())),t.addEventListener("pointerenter",o(e=>i())),t.addEventListener("pointerleave",o(e=>s()))}})})</script><main><style>body{background-color:var(--background-color)}h1{margin-bottom:8px;margin-top:32px}:not(pre)>code{background:var(--code-background-color);padding:1px 4px;font-size:.95em;border-radius:3px}pre{background:var(--codeblock-background-color);color:var(--codeblock-color)}pre code{display:block;overflow-x:auto;padding:.5em}.date-big{line-height:2;margin-left:32px}article{background-color:var(--base-color);box-shadow:0 3px 2px rgba(0,0,0,.15)}figure{margin:0}figure img{margin:0}article img,article video{max-width:100%;height:auto;display:block;margin:auto;margin-top:16px;margin-bottom:16px}article h1{margin-top:64px}article h2,article h3,article h4{margin-top:42px}.article-info{display:flex;flex-direction:column;gap:8px}.article-metadata{display:flex;gap:24px;align-items:center;font-family:var(--header-font-family);margin-bottom:12px}@media(max-width:900px){.article-metadata{flex-direction:column;align-items:flex-start;gap:16px}}.article-author{color:var(--base-color-text-subtitle-date);font-weight:700;font-size:18px;flex-grow:1;display:flex;gap:12px;align-items:center}.article-author .avatar{border-radius:100%;margin:0;background:0 0}.article-author .by{color:var(--base-color-text-subtitle)}.article-metadata .date{color:var(--base-color-text-subtitle-date)}.article-metadata .date.post-recent-highlight{color:var(--post-recent-highlight-color);opacity:.8}.article-metadata .date.post-recent-highlight::after{font-size:80%;content:"NEW";border:2px solid var(--post-recent-highlight-color);padding:2px 3px;margin-left:8px}.tag.active{filter:saturate(.75)}@media screen and (min-width:900px){article .content{width:70%;margin:auto}}@media(max-width:900px){body{background-color:var(--base-color)}article{background-color:initial;box-shadow:none}article img:first-child,article video:first-child{max-width:100%}}.blog-navigation{display:grid;grid-template-columns:1fr 1fr;padding-top:30px;padding-bottom:60px}.blog-navigation .next{text-align:right}@media(max-width:900px){.blog-navigation{grid-template-columns:1fr;gap:20px;border-top:1px solid var(--code-background-color)}.blog-navigation .next{text-align:left}}.blog-navigation span{opacity:.6;font-weight:700;margin-bottom:5px;display:block}.blog-navigation a{display:inline-block;text-decoration:none;color:inherit;opacity:.6;transition:opacity .2s}.blog-navigation a:hover{opacity:1}</style><link rel=stylesheet href=/assets/css/highlight.obsidian.min.css><div class=container><article class=padded><div class="content article-container"><figure class=article-cover><img src=/storage/app/uploads/public/5eb/018/a65/5eb018a659a8f843228718.png title alt=" " class=rounded-lg style=width:100%;height:auto;background-color:initial></figure><div class=article-info><h1>GDScript progress report: Writing a tokenizer</h1><div class=article-metadata><div class=article-author><span>By: </span><img class=avatar width=25 height=25 src=/assets/images/authors/vnen.webp alt="George Marques" loading=lazy>
<span class=by>George Marques</span></div><span class=date data-post-date="2020-05-04 11:00:00 +0000">4 May 2020</span></div><div class=tags><a href=/blog/progress-report><div class="tag active">Progress Report</div></a></div></div><div class="card card-warning"><p>This article is from <strong>May 2020</strong>, some of its contents might be outdated and no longer accurate.<br>You can find up-to-date information about the engine in the <a href=https://docs.godotengine.org/en/stable/>official documentation</a>.</div><div class=article-body><p>Some of you may not be aware, but Im currently rewriting GDScript. It was discussed during the last Godot Sprint in Brussels and the core developers approved the idea.<p>The main rationale to rewrite the GDScript implementation is that it has been changed so much that its gotten quite hard to understand after receiving so many new features. This is also an opportunity to clean up, tidy up, and modernize the code, like it happened for some other parts of the engine that got rewritten in the past years.<p>The objective is to have a more “textbook-like” compiler, which will help maintenance both by those already acquainted with the Godot source and by new faces who saw something about writing compilers in the past. Hopefully this will make it harder to mess something up, given this is critical code for the engine users.<p><em>See other articles in this Godot 4.0 GDScript series:</em><ol><li>(you are here) <a href=https://godotengine.org/article/gdscript-progress-report-writing-tokenizer>GDScript progress report: Writing a tokenizer</a><li><a href=https://godotengine.org/article/gdscript-progress-report-writing-new-parser>GDScript progress report: Writing a new parser</a><li><a href=https://godotengine.org/article/gdscript-progress-report-type-checking-back>GDScript progress report: Type checking is back</a><li><a href=https://godotengine.org/article/gdscript-progress-report-new-gdscript-now-merged>GDScript progress report: New GDScript is now merged</a><li><a href=https://godotengine.org/article/gdscript-progress-report-typed-instructions>GDScript progress report: Typed instructions</a><li><a href=https://godotengine.org/article/gdscript-progress-report-feature-complete-40>GDScript progress report: Feature-complete for 4.0</a></ol><h2 id=why-not-a-parser-generator>Why not a parser generator?</h2><p>Ive seen this question more than once: why cant we use a parser generator? Then we would just need to write a grammar specification and the generator makes the whole parser for us.<p>The main problem with parser generators—and the main reason modern language compilers dont use them—is that it is incredibly hard to provide meaningful error messages to the users of the language. With hand-made parsers we can inject checks, such as properly validating the path you put in a <code class="language-plaintext highlighter-rouge">preload</code> statement, among other things.<p>The GDScript grammar is also pretty much settled (except maybe for a few of the new features) so we dont benefit from prototyping and iterating on a grammar definition, which is the main selling point of parser generators.<h2 id=rewriting-the-tokenizer>Rewriting the tokenizer</h2><p>The first step when writing a compiler is to make the <em>tokenizer</em> (also called <em>scanner</em> by some authors). It has the responsibility of reading the characters of the source code and bundling them together as meaningful chunks which are called <em>tokens</em>. Tokens contain information about what they mean and where they occured. This makes the following compilation steps easier to manage as they dont have to deal with minutiae such as comments and whitespace, which are not meaningful for the final execution.<p>Im taking this opportunity to make the tokenizer a bit smarter (and maybe a bit dumber in some regards) in order to make the parsing simpler (Ill write about the parser when I get there). The new tokenizer will emit special tokens when it detects theres an indentation change and also a newline. Since GDScript is indentation based, this helps the parser identify the start and end of blocks. This approach is also used by Python, so Im not being revolutionary here.<p>Im also rewriting the <code class="language-plaintext highlighter-rouge">--test gd*</code> commands so they work with the new code. Those are very useful during this rewrite since they allow me test the steps of the compiler without needing to have everything ready. I can make sure the tokenizer is working properly before moving on to the parser.<h2 id=what-is-a-token>What is a token?</h2><p>A <strong>token</strong> is a data container which describes the characters in the source code as something meaningful for the programming language. The main thing to store is the token type (e.g. a symbol, an identifier, a keyword, etc.), which is a value taken from an enum, since there are limited variety. This is used by the parser to understand which statement to expect.<p>Another thing is the potential associated data. If its, say, a <code class="language-plaintext highlighter-rouge">+</code> sign it doesnt matter since the type <code class="language-plaintext highlighter-rouge">Token::PLUS</code> is enough to recognize it, but a <code class="language-plaintext highlighter-rouge">Token::IDENTIFIER</code> needs to have the actual identifier to be referred. Literal values, such as numbers and strings, also need to be stored. The new tokenizer uses a single Variant to store this.<p>Its also useful to store positional information about the token: line/column where it starts and ends. This is not useful for interpreting the code, but is incredibly valuable for creating readable error messages.<h2 id=features-of-the-new-tokenizer>Features of the new tokenizer</h2><p>As mentioned, the main reason for the rewrite is to make it smarter, so let me show off some of the new features.<h3 id=storing-correct-positional-data>Storing correct positional data</h3><p>The old tokenizer stored only line and column, but there are many tokens that span across many columns and some across multiple lines. For example, a snippet like this one:<div class="language-gdscript highlighter-rouge"><div class=highlight><pre class=highlight><code><span class=k>var</span> <span class=n>x</span> <span class=o>=</span> <span class=s2>"string"</span>
<span class=n>x</span> <span class=o>+=</span> <span class=s2>"""</span><span class=se>\
</span><span class=s2>Multiline String
"""</span>
</code></pre></div></div><p>Can now be tokenized like this:<div class="language-plaintext highlighter-rouge"><div class=highlight><pre class=highlight><code>0001 var x = "string"
^^^
--&gt; var
-------------------------------------------------------
0001 var x = "string"
^
--&gt; Identifier(StringName) x
-------------------------------------------------------
0001 var x = "string"
^
--&gt; =
-------------------------------------------------------
0001 var x = "string"
^^^^^^^^
--&gt; Literal(String) string
-------------------------------------------------------
0001 var x = "string"
^
--&gt; Newline
-------------------------------------------------------
0002 x += """\
^
--&gt; Identifier(StringName) x
-------------------------------------------------------
0002 x += """\
^^
--&gt; +=
-------------------------------------------------------
0002 x += """\
0003 Multiline String
0004 """
^^^^^^^^^^^^^^^^
--&gt; Literal(String) Multiline String
-------------------------------------------------------
0004 """
^
--&gt; Newline
-------------------------------------------------------
EOF
</code></pre></div></div><p>This is actually the output of the new tokenizer test. It rewrites the lines and points to the whole span of the token. This is meant to improve error messages by pointing the user to the exact spot.<h3 id=creating-indentation-and-newline-tokens>Creating indentation and newline tokens</h3><p>While the previous tokenizer kept track of indentation characters and newlines, it let the parser decide the current indentation level, and whether or not a newline was relevant. The new one only generates a newline token when its not empty, and also emits tokens for indentation. For example:<div class="language-gdscript highlighter-rouge"><div class=highlight><pre class=highlight><code><span class=s2>"start"</span>
<span class=s2>"indent"</span>
<span class=s2>"more indent"</span>
<span class=s2>"two dedents"</span>
</code></pre></div></div><p>Is tokenized like this:<div class="language-plaintext highlighter-rouge"><div class=highlight><pre class=highlight><code>0001 "start"
^^^^^^^
--&gt; Literal(String) start
-------------------------------------------------------
0001 "start"
^
--&gt; Newline
-------------------------------------------------------
0003 "indent"
^^^^
--&gt; Indent
-------------------------------------------------------
0003 "indent"
^^^^^^^^
--&gt; Literal(String) indent
-------------------------------------------------------
0003 "indent"
^
--&gt; Newline
-------------------------------------------------------
0004 "more indent"
^^^^^^^^
--&gt; Indent
-------------------------------------------------------
0004 "more indent"
^^^^^^^^^^^^^
--&gt; Literal(String) more indent
-------------------------------------------------------
0004 "more indent"
^
--&gt; Newline
-------------------------------------------------------
0005 "two dedents"
^
--&gt; Dedent
-------------------------------------------------------
0005 "two dedents"
^
--&gt; Dedent
-------------------------------------------------------
0005 "two dedents"
^^^^^^^^^^^^^
--&gt; Literal(String) two dedents
-------------------------------------------------------
0005 "two dedents"
^
--&gt; Newline
-------------------------------------------------------
EOF
</code></pre></div></div><p>You can see that it detects a decrease in two levels by emitting two <code class="language-plaintext highlighter-rouge">dedent</code> tokens. It also doesnt emit a newline on line 2 because its empty.<h3 id=error-reporting-and-leniency>Error reporting and leniency</h3><p>The old tokenizer only allowed for one error, after which it would only return the same error if you asked for a new token. Now it returns the error but keeps going through the process so you can detect many errors at once. So this:<div class="language-gdscript highlighter-rouge"><div class=highlight><pre class=highlight><code><span class=s2>"invalid escape \h &lt;- here"</span>
<span class=s2>"indent"</span>
<span class=s2>"mismatched unindent"</span>
</code></pre></div></div><p>Gives this:<div class="language-plaintext highlighter-rouge"><div class=highlight><pre class=highlight><code>0001 "invalid escape \h &lt;- here"
^^^^^^^^^^^^^^^^^^^^^^^^^^^
--&gt; Literal(String) invalid escape &lt;- here
-------------------------------------------------------
0001 "invalid escape \h &lt;- here"
^^
--&gt; Error(String) Invalid escape in string.
-------------------------------------------------------
0001 "invalid escape \h &lt;- here"
^
--&gt; Newline
-------------------------------------------------------
0002 "indent"
^^^^^^^^
--&gt; Indent
-------------------------------------------------------
0002 "indent"
^^^^^^^^
--&gt; Literal(String) indent
-------------------------------------------------------
0002 "indent"
^
--&gt; Newline
-------------------------------------------------------
0003 "mismatched unindent"
^^^^^
--&gt; Error(String) Unindent doesn't match the previous indentation level.
-------------------------------------------------------
0003 "mismatched unindent"
^^^^^
--&gt; Dedent
-------------------------------------------------------
0003 "mismatched unindent"
^^^^^^^^^^^^^^^^^^^^^
--&gt; Literal(String) mismatched unindent
-------------------------------------------------------
0003 "mismatched unindent"
^
--&gt; Newline
-------------------------------------------------------
0004
^
--&gt; Dedent
-------------------------------------------------------
EOF
</code></pre></div></div><p>You can notice that the error in the string comes after the string token. Thats okay because the parser will report all errors at once and they will be sorted by position.<p>Indentation level is also correctly maintained even in case of a mismatch (thats why theres two dedents for one indent). This is helpful to keep the parsing lenient in some special cases, which Ill talk more about when I write about the parser.<h3 id=recognizing-extra-tokens>Recognizing extra tokens</h3><p>Some characters and sequences arent allowed in the source code but they can be tokenized so a better error message can be provided. Example:<div class="language-plaintext highlighter-rouge"><div class=highlight><pre class=highlight><code>? ` &lt;&lt;&lt;&lt;&lt;&lt;&lt; ======= &gt;&gt;&gt;&gt;&gt;&gt;&gt;
</code></pre></div></div><p>Result:<div class="language-plaintext highlighter-rouge"><div class=highlight><pre class=highlight><code>0001 ? ` &lt;&lt;&lt;&lt;&lt;&lt;&lt; ======= &gt;&gt;&gt;&gt;&gt;&gt;&gt;
^
--&gt; ?
-------------------------------------------------------
0001 ? ` &lt;&lt;&lt;&lt;&lt;&lt;&lt; ======= &gt;&gt;&gt;&gt;&gt;&gt;&gt;
^
--&gt; `
-------------------------------------------------------
0001 ? ` &lt;&lt;&lt;&lt;&lt;&lt;&lt; ======= &gt;&gt;&gt;&gt;&gt;&gt;&gt;
^^^^^^^
--&gt; VCS conflict marker
-------------------------------------------------------
0001 ? ` &lt;&lt;&lt;&lt;&lt;&lt;&lt; ======= &gt;&gt;&gt;&gt;&gt;&gt;&gt;
^^^^^^^
--&gt; VCS conflict marker
-------------------------------------------------------
0001 ? ` &lt;&lt;&lt;&lt;&lt;&lt;&lt; ======= &gt;&gt;&gt;&gt;&gt;&gt;&gt;
^^^^^^^
--&gt; VCS conflict marker
-------------------------------------------------------
0001 ? ` &lt;&lt;&lt;&lt;&lt;&lt;&lt; ======= &gt;&gt;&gt;&gt;&gt;&gt;&gt;
^
--&gt; Newline
-------------------------------------------------------
EOF
</code></pre></div></div><p>The question mark can be recognized as an attempt to use the C-style ternary conditional operator, so we can show the user the correct operator for GDScript.<p>We also recognize conflict markers so you can easily notice that you have a merge conflict in the file.<h2 id=what-comes-next>What comes next</h2><p>Now that the tokenizer is pretty much done, Ill start rewriting the parser, which is responsible for reading the sequence of tokens and making sense out of them, deciding if its a function, variable, expression, etc. Ill write more about it when I have something to show.</div></div></article><div class=blog-navigation><div class=previous><span>Previous</span>
<a rel=prev href=/article/vulkan-progress-report-7/>Vulkan progress report #7</a></div><div class=next><span>Next</span>
<a rel=next href=/article/dev-snapshot-godot-3-2-2-beta-2/>Dev snapshot: Godot 3.2.2 beta 2</a></div></div></div><link rel=stylesheet href=/assets/css/anchor-link.css?1><link rel=stylesheet href=/assets/css/article-cards.css?3><script src=/assets/js/anchor-link.js></script><script>document.addEventListener("DOMContentLoaded",()=>{window.applyAnchorLinks(".article-body"),document.querySelectorAll(".article-cover img, .article-body img").forEach(e=>{if(e.classList.contains("lightbox-ignore"))return;const t=document.createElement("a");t.href=e.src,t.classList.add("lightbox"),t.dataset.group="article",e.parentNode.appendChild(t),t.appendChild(e)})})</script></main><footer class=footer-global><div class=wrapper><div class=columns><div class=col><h2>Godot Engine</h2><ul><li><a class=set-os-download-url href=/download>Download</a><li><a href=https://docs.godotengine.org>Documentation</a><li><a href=/features/>Features</a><li><a href=https://editor.godotengine.org/releases/latest/>Web editor</a><li><a href=/download/archive/>Release archive</a><li><a href=https://github.com/godotengine>Source code</a></ul></div><div class=col><h2>Project</h2><ul><li><a href=/blog/>Blog</a><li><a href=/code-of-conduct/>Code of conduct</a><li><a href=/governance/>Governance</a><li><a href=/teams/>Teams</a><li><a href=/priorities/>Priorities</a><li><a href=/community/>Communities</a></ul></div><div class=col><h2>Resources</h2><ul><li><a href=https://godotengine.org/asset-library/asset>Asset library</a><li><a href=/press/>Press kit</a><li><a href=/showcase/>Showcase</a><li><a href=/education/>Education</a><li><a href=/consoles/>Console support</a></ul></div><div class=col><h2>Foundation</h2><ul><li><a href=https://godot.foundation/>About</a><li><a href=https://fund.godotengine.org>Donate</a><li><a href=/license/>License</a><li><a href=/privacy-policy/>Privacy policy</a><li><a href=/contact/>Contact us</a></ul></div></div><hr><div class=credits-and-socials><p>© 2007-2026 Juan Linietsky, Ariel Manzur and <a href=https://github.com/godotengine/godot/blob/master/AUTHORS.md target=_blank rel=noopener>contributors</a>. Hosted by the <a href=https://godot.foundation/ target=_blank rel=noopener>Godot Foundation</a>. Website <a href=https://github.com/godotengine/godot-website target=_blank rel=noopener>source code on GitHub</a>.<div class=social><a href=https://github.com/godotengine target=_blank rel=noopener title=GitHub><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6.0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6.0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3.0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1.0-6.2-.3-40.4-.3-61.4.0.0-70 15-84.7-29.8.0.0-11.4-29.1-27.8-36.6.0.0-22.9-15.7 1.6-15.4.0.0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5.0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9.0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4.0 33.7-.3 75.4-.3 83.6.0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6.0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9.0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>
</a><a href=https://bsky.app/profile/godotengine.org target=_blank rel=noopener title=Bluesky><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M407.8 294.7c-3.3-.4-6.7-.8-10-1.3 3.4.4 6.7.9 10 1.3zM288 227.1C261.9 176.4 190.9 81.9 124.9 35.3 61.6-9.4 37.5-1.7 21.6 5.5 3.3 13.8.0 41.9.0 58.4S9.1 194 15 213.9c19.5 65.7 89.1 87.9 153.2 80.7 3.3-.5 6.6-.9 10-1.4-3.3.5-6.6 1-10 1.4-93.9 14-177.3 48.2-67.9 169.9C220.6 589.1 265.1 437.8 288 361.1c22.9 76.7 49.2 222.5 185.6 103.4 102.4-103.4 28.1-156-65.8-169.9-3.3-.4-6.7-.8-10-1.3 3.4.4 6.7.9 10 1.3 64.1 7.1 133.6-15.1 153.2-80.7C566.9 194 576 75 576 58.4s-3.3-44.7-21.6-52.9c-15.8-7.1-40-14.9-103.2 29.8C385.1 81.9 314.1 176.4 288 227.1z"/></svg>
</a><a href=https://mastodon.gamedev.place/@godotengine target=_blank rel=noopener title=Mastodon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M433 179.1c0-97.2-63.7-125.7-63.7-125.7-62.5-28.7-228.6-28.4-290.5.0.0.0-63.7 28.5-63.7 125.7.0 115.7-6.6 259.4 105.6 289.1 40.5 10.7 75.3 13 103.3 11.4 50.8-2.8 79.3-18.1 79.3-18.1l-1.7-36.9s-36.3 11.4-77.1 10.1c-40.4-1.4-83-4.4-89.6-54a102.5 102.5.0 01-.9-13.9c85.6 20.9 158.7 9.1 178.8 6.7 56.1-6.7 105-41.3 111.2-72.9 9.8-49.8 9-121.5 9-121.5zm-75.1 125.2h-46.6V190.1c0-49.7-64-51.6-64 6.9v62.5H201V197c0-58.5-64-56.6-64-6.9v114.2H90.2c0-122.1-5.2-147.9 18.4-175 25.9-28.9 79.8-30.8 103.8 6.1l11.6 19.5 11.6-19.5c24.1-37.1 78.1-34.8 103.8-6.1 23.7 27.3 18.4 53 18.4 175z"/></svg>
</a><a href=https://discord.gg/godotengine target=_blank rel=noopener title=Discord><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M524.5 69.8a1.5 1.5.0 00-.8-.7A485.1 485.1.0 00404.1 32a1.8 1.8.0 00-1.9.9 337.5 337.5.0 00-14.9 30.6 447.8 447.8.0 00-134.4.0 309.5 309.5.0 00-15.1-30.6 1.9 1.9.0 00-1.9-.9A483.7 483.7.0 00116.1 69.1a1.7 1.7.0 00-.8.7C39.1 183.7 18.2 294.7 28.4 404.4a2 2 0 00.8 1.4A487.7 487.7.0 00176 479.9a1.9 1.9.0 002.1-.7 348.2 348.2.0 0030-48.8 1.9 1.9.0 00-1-2.6 321.2 321.2.0 01-45.9-21.9 1.9 1.9.0 01-.2-3.1c3.1-2.3 6.2-4.7 9.1-7.1a1.8 1.8.0 011.9-.3c96.2 43.9 200.4 43.9 295.5.0a1.8 1.8.0 011.9.2c2.9 2.4 6 4.9 9.1 7.2a1.9 1.9.0 01-.2 3.1 301.4 301.4.0 01-45.9 21.8 1.9 1.9.0 00-1 2.6 391.1 391.1.0 0030 48.8 1.9 1.9.0 002.1.7 486 486 0 00147.2-74.1 1.9 1.9.0 00.8-1.4c12.2-126.7-20.6-236.8-87-334.5zm-302 267.8c-29 0-52.8-26.6-52.8-59.2s23.4-59.3 52.8-59.3c29.7.0 53.3 26.8 52.8 59.2.0 32.7-23.4 59.3-52.8 59.3zm195.4.0c-29 0-52.8-26.6-52.8-59.2s23.3-59.3 52.8-59.3c29.7.0 53.3 26.8 52.8 59.2.0 32.7-23.2 59.3-52.8 59.3z"/></svg>
</a><a href=https://www.reddit.com/r/godot title=Reddit target=_blank rel=noopener><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 256C0 114.6 114.6.0 256 0S512 114.6 512 256 397.4 512 256 512H37.1c-13.7.0-20.5-16.5-10.9-26.2L75 437C28.7 390.7.0 326.7.0 256zM349.6 153.6c23.6.0 42.7-19.1 42.7-42.7s-19.1-42.7-42.7-42.7c-20.6.0-37.8 14.6-41.8 34-34.5 3.7-61.4 33-61.4 68.4v.2c-37.5 1.6-71.8 12.3-99 29.1-10.1-7.8-22.8-12.5-36.5-12.5-33 0-59.8 26.8-59.8 59.8.0 24 14.1 44.6 34.4 54.1 2 69.4 77.6 125.2 170.6 125.2s168.7-55.9 170.6-125.3c20.2-9.6 34.1-30.2 34.1-54 0-33-26.8-59.8-59.8-59.8-13.7.0-26.3 4.6-36.4 12.4-27.4-17-62.1-27.7-1e2-29.1v-.2c0-25.4 18.9-46.5 43.4-49.9 4.4 18.8 21.3 32.8 41.5 32.8zM177.1 246.9c16.7.0 29.5 17.6 28.5 39.3s-13.5 29.6-30.3 29.6-31.4-8.8-30.4-30.5S160.3 247 177 247zm190.1 38.3c1 21.7-13.7 30.5-30.4 30.5s-29.3-7.9-30.3-29.6c-1-21.7 11.8-39.3 28.5-39.3s31.2 16.6 32.1 38.3zm-48.1 56.7c-10.3 24.6-34.6 41.9-63 41.9s-52.7-17.3-63-41.9c-1.2-2.9.8-6.2 3.9-6.5 18.4-1.9 38.3-2.9 59.1-2.9s40.7 1 59.1 2.9c3.1.3 5.1 3.6 3.9 6.5z"/></svg>
</a><a href=/rss.xml title=RSS target=_blank rel=noopener><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M64 32C28.7 32 0 60.7.0 96V416c0 35.3 28.7 64 64 64h320c35.3.0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24 137 0 248 111 248 248 0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-2e2-2e2-2e2-13.3.0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24 83.9.0 152 68.1 152 152 0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104-13.3.0-24-10.7-24-24zm0 120a32 32 0 1164 0 32 32 0 11-64 0z"/></svg></a></div></div></div></footer><script defer src=/assets/js/localize.js?7></script><script defer src=/assets/js/tobii.min.js></script><script defer src=/assets/js/highlight.min.js?1></script><script defer src=/assets/js/highlight.gdscript.min.js?1></script><script>document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll("pre:not(.manual) code").forEach(e=>{hljs.highlightBlock(e)}),document.querySelectorAll("[data-post-date]").forEach(e=>{Date.parse(e.dataset.postDate)>Date.now()-1e3*60*60*48&&e.classList.add("post-recent-highlight")}),new Tobii({zoom:!1});const e=document.querySelectorAll(".set-os-download-url");for(let n=0;n<e.length;n++){const s=e[n];let o="download";"version"in s.dataset&&s.dataset.version==="3"&&(o="download/3.x");let t="windows";navigator.platform.indexOf("Mac")!==-1?t="macos":navigator.userAgent.indexOf("Android")!==-1?t="android":navigator.platform.indexOf("Linux")!==-1&&(t="linux"),s.href=`/${o}/${t}/`}})</script>