Files
godot-website/article/multiplayer-changes-godot-4-0-report-4/index.html
2025-03-14 11:43:18 +00:00

111 lines
28 KiB
HTML
Raw 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="The long-awaited first post about the multiplayer replication system in development for Godot 4.0 is here! Check out the design goals, concepts, initial prototype, and as always, stay tuned for more!"><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/multiplayer-changes-godot-4-0-report-4/"><meta property="og:type" content="website"><meta property="og:description" content="The long-awaited first post about the multiplayer replication system in development for Godot 4.0 is here! Check out the design goals, concepts, initial prototype, and as always, stay tuned for more!"><meta property="og:image" content="https://godotengine.org/storage/app/uploads/public/61a/27f/881/61a27f8816a4e934017559.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/multiplayer-changes-godot-4-0-report-4/"><meta property="og:title" content="Multiplayer in Godot 4.0: Scene Replication (part 1) Godot Engine"><title>Multiplayer in Godot 4.0: Scene Replication (part 1) 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?1><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/>Features</a><li><a href=/showcase/>Showcase</a><li><a href=/blog/>Blog</a><li><a href=/community/>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://docs.godotengine.org/en/stable/contributing/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><script>document.addEventListener("click",function(e){const t=document.querySelector(".language-selector");if(!t)return;t.contains(e.target)||t.classList.remove("open")});function setLanguagePreference(e,t){e.preventDefault();const s=t.getAttribute("data-lang-path"),o=t.getAttribute("data-lang"),n=new Date;n.setDate(n.getDate()+365),document.cookie=`preferred_language=${o}; expires=${n.toUTCString()}; path=/; SameSite=Lax`,window.location.href=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/61a/27f/881/61a27f8816a4e934017559.png title alt=" " class=rounded-lg style=width:100%;height:auto;background-color:initial></figure><div class=article-info><h1>Multiplayer in Godot 4.0: Scene Replication (part 1)</h1><div class=article-metadata><div class=article-author><span>By: </span><img class=avatar width=25 height=25 src=/assets/images/authors/faless.webp alt="Fabio Alessandrelli" loading=lazy>
<span class=by>Fabio Alessandrelli</span></div><span class=date data-post-date="2021-11-27 19:00:00 +0000">27 November 2021</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>November 2021</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>Howdy Godotters!<p>Its finally time for the long-awaited post about the new multiplayer replication system that is being developed for Godot 4.0.
Below, we will introduce the concepts around which it was designed, the currently implemented prototype, and planned changes to make it more powerful and user-friendly.<p><em>See other articles in this Godot 4.0 networking series:</em><ol><li><a href=https://godotengine.org/article/multiplayer-changes-godot-4-0-report-1>Multiplayer in Godot 4.0: On servers, RSETs and state updates</a><li><a href=https://godotengine.org/article/multiplayer-changes-godot-4-0-report-2>Multiplayer in Godot 4.0: RPC syntax, channels, ordering</a><li><a href=https://godotengine.org/article/multiplayer-changes-godot-4-0-report-3>Multiplayer in Godot 4.0: ENet wrappers, WebRTC</a><li>(you are here) <a href=https://godotengine.org/article/multiplayer-changes-godot-4-0-report-4>Multiplayer in Godot 4.0: Scene Replication (part 1)</a></ol><h2 id=design-goals>Design goals</h2><p>Making multiplayer games has historically been a complex task, requiring ad-hoc optimizations and game-specific solutions. Still, two main concepts are almost ubiquitous in multiplayer games: some form of <strong>messaging</strong>, and some form of <strong>state replication</strong> (synchronization and reconciliation).<p>While Godot does provide a system for messaging (i.e. <abbr title="Remote Procedure Calls">RPC</abbr>), it does not provide a common system for replication.<p>In this sense, we had quite a few <a href=https://chat.godotengine.org/>#networking meetings</a> in August 2021 to design a replication API that could be used for the common cases, while being extensible via plugins or custom code.<p>The design goals that emerged for such an API where:<ul><li>Provide an out-of-the-box solution for scene state replication across the network.<li>Allow for (almost) no-code prototyping.<li>Be extensible with game-specific behaviours (custom reconciliation, interpolation, interest management, etc).<li>Allow ex-post (incremental) optimizations of network code.<li>Be easy to use for game developers, of course :)</ul><h3 id=glossary>Glossary</h3><ul><li><code class="language-plaintext highlighter-rouge">State</code>: The informations (properties) about an Object relevant to the multiplayer game.<li><code class="language-plaintext highlighter-rouge">Spawn</code>: Creating, or requesting remotely to create a new Object.<li><code class="language-plaintext highlighter-rouge">Sync</code>: Updating, or requesting remotely to update the state of an Object.</ul><h3 id=security>Security</h3><p>When dealing with computer networks, its important to understand the security implication of transfering data across machines.
For instance, Godot does not allow <a href=https://docs.godotengine.org/en/stable/classes/class_multiplayerapi.html#class-multiplayerapi-property-allow-object-decoding>decoding objects</a> by default, since they could carry scripts with them or force the receiving end to execute specific code during initialization. This is a security vulnerability, as arbitrary code execution of this kind would allow for servers to access or manipulate any file on the clients filesystem that the game process has access to.<p>In a similar way, the replication API will let you specify which scenes can be spawned by a remote peer. Tthe final implementation will also allow for fine-grained control over which node can be spawned at each specific path.<h3 id=optimizations>Optimizations</h3><p>Optimizations, and bandwidth optimizations in particular, are crucial to an effective networking protocol.<ul><li>Synchronizing multiple properties is very useful in the prototyping stage, but bad in terms of potential optimizations.<li>A very quick way to optimize the network code later on is to replicate a single property that returns a tightly packed representation of the object state based on your games unique characteristics.
When done properly, this is also going to be the most optimized state possible that no tool can produce for you.<li>The replication API will still try to squeeze the state size as much as possible with the information in its hands.</ul><h2 id=initial-prototype>Initial prototype</h2><p>With this in mind, an initial prototype was developed and has been merged in Godots <code class="language-plaintext highlighter-rouge">master</code> branch.
Please note that <strong>the final implementation will be substantially different</strong> in terms of exposed low-level API. Nonetheless, it will retain the same concepts and functionalities while adding more as we gather more feedback (jump to the next section for more information).<p>The initial prototype requires some wiring via GDScript, but <strong>the final version will use visual configuration nodes</strong> for better usability.<p>Without further ado, lets create our player:<div class="language-plaintext highlighter-rouge"><div class=highlight><pre class=highlight><code># player.gd
extends CharacterBody2D
# The player name.
var player_name: String
func _ready():
print("Player spawned. Name: %s, position: %s" % [player_name, position])
func _notification(what):
if what == NOTIFICATION_PREDELETE:
print("Player deleted. Name: %s" % player_name)
</code></pre></div></div><p>Now lets create our main scene, which configures the replication, and starts the networking:<div class="language-plaintext highlighter-rouge"><div class=highlight><pre class=highlight><code># main.gd
extends Node
# The player scene (which we want to configure for replication).
const Player = preload("res://player.tscn")
func _ready():
# Get the UID of the scene we want replicated.
var id = ResourceLoader.get_resource_uid(Player.resource_path)
# Configure the scene to be controlled by the server,
# and which properties will be replicated during spawn.
multiplayer.replicator.spawn_config(id, MultiplayerReplicator.REPLICATION_MODE_SERVER,
[&amp;"player_name", &amp;"position"])
# Configure the variables to be synchronized periodically
# (every 16 milliseconds = 62.5 Hz).
multiplayer.replicator.sync_config(id, 16, [&amp;"position"])
# Start the server if Godot is passed the "--server" argument,
# and start a client otherwise.
if "--server" in OS.get_cmdline_args():
start_network(true)
else:
start_network(false)
func start_network(server: bool):
var peer = ENetMultiplayerPeer.new()
if server:
# Listen to peer connections, and create new player for them
multiplayer.peer_connected.connect(self.create_player)
# Listen to peer disconnections, and destroy their players
multiplayer.peer_disconnected.connect(self.destroy_player)
peer.create_server(4242)
else:
peer.create_client("localhost", 4242)
multiplayer.set_multiplayer_peer(peer)
func create_player(id):
# Instantiate a new player for this client.
var p = Player.instantiate()
# Sets the player name (only sent during spawn).
p.player_name = "Player %d" % id
# Set a random position (sent on every replicator update).
p.position = Vector2(randi() % 500, randi() % 500)
# Add it to the "Players" node.
# We give the new Node a name for easy retrieval, but that's not necessary.
p.name = str(id)
$Players.add_child(p)
func destroy_player(id):
# Delete this peer's node.
$Players.get_node(str(id)).queue_free()
</code></pre></div></div><p>With this configuration, each new client that connects will cause the server to instantiate a new player for it.<p>Note that the client code doesnt “instantiates” the scene explicitly. However, since the scene is marked for replication, when the server adds the scene to the SceneTree, it automatically sends that information remotely. Each connected client will then instantiate the scene automatically, adding it to the proper path and setting the values configured via <code class="language-plaintext highlighter-rouge">multiplayer.replicator.spawn_config</code> (<code class="language-plaintext highlighter-rouge">position</code> and <code class="language-plaintext highlighter-rouge">player_name</code> in this example).<p>Additinally, the server automatically keeps track of replicated nodes to send them to newly connected peers, i.e. supporting clients that join mid-game.<p>The RPC system will also work appropriately for the nodes spawned this way, so you can easily integrate state synchronization with messaging.<p>At the specified interval (16 milliseconds in the above example), the properties passed to <code class="language-plaintext highlighter-rouge">multiplayer.replicator.sync_config</code> will also be synchronized from the server to the client.<p>You can decide to synchronize multiple properties via <code class="language-plaintext highlighter-rouge">sync_config</code>, but keep in mind that will result in a larger sync state. If the sync state becomes too large, this can potentially introduce latency or packet loss.<div class="language-plaintext highlighter-rouge"><div class=highlight><pre class=highlight><code>multiplayer.replicator.sync_config(id, 16, [&amp;"position", &amp;"health", &amp;"mana"])
</code></pre></div></div><p>In those cases, a good way to optimize the state is to use a dedicated “sync_state” property with your own optimized representation:<div class="language-plaintext highlighter-rouge"><div class=highlight><pre class=highlight><code>multiplayer.replicator.sync_config(id, 16, [&amp;"sync_state"])
</code></pre></div></div><p>And then in your player script:<div class="language-plaintext highlighter-rouge"><div class=highlight><pre class=highlight><code># player.gd
# In this example, health and mana must be set between 0 and 255
# to be encoded as 8-bit integers.
var health := 100
var mana := 100
# Optimized state representation using bit-packing.
var sync_state:
get:
var buf = PackedByteArray()
buf.resize(6)
buf.encode_half(0, position.x)
buf.encode_half(2, position.y)
buf.encode_u8(4, health)
buf.encode_u8(5, mana)
return buf
set(value):
assert(typeof(value) == TYPE_RAW_ARRAY and value.size() == 6,
"Invalid `sync_state` array type or size (must be TYPE_RAW_ARRAY of size 6).")
position = Vector2(value.decode_half(0), value.decode_half(2))
health = value.decode_u8(4)
mana = value.decode_u8(5)
</code></pre></div></div><p>In the same way, properties of child nodes could be set, and custom interpolation techniques implemented.<h2 id=future-work>Future work</h2><p>As explained, this is an early prototype. A <a href=https://github.com/godotengine/godot-proposals/issues/3459>more complete proposal</a> has been created to gather feedback as we work towards a final implementation in the coming months. This includes <strong>visual configuration</strong>, child node properties support, fine grained spawn control and more.<p>The coming months will be prety dense of announcements. As always, stay tuned for more!<h2 id=references>References</h2><ul><li><a href=https://github.com/godotengine/godot/pull/51097>Spawn/Despawn pull request</a><li><a href=https://github.com/godotengine/godot/pull/51534>Spawn/Despawn initial state pull request</a><li><a href=https://github.com/godotengine/godot/pull/51788>State synchronization pull request</a><li><a href=https://github.com/godotengine/godot-proposals/issues/3459>Last replication proposal</a> (ongoing).</ul></div></div></article><div class=blog-navigation><div class=previous><span>Previous</span>
<a rel=prev href=/article/release-candidate-godot-3-4-1-rc-1/>Release candidate: Godot 3.4.1 RC 1</a></div><div class=next><span>Next</span>
<a rel=next href=/article/release-candidate-godot-3-4-1-rc-2/>Release candidate: Godot 3.4.1 RC 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></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-2025 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?3></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 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 t=0;t<e.length;t++){const n=e[t];let o="download";"version"in n.dataset&&n.dataset.version==="3"&&(o="download/3.x");let s="windows";navigator.platform.indexOf("Mac")!==-1?s="macos":navigator.platform.indexOf("Linux")!==-1&&(s="linux"),n.href=`/${o}/${s}/`}})</script>