avatarMina Pêcheux

Summary

This tutorial explains how to create a 3D character controller with animations in Godot 4 using C#, Blender, and Mixamo.

Abstract

This tutorial provides a step-by-step guide on how to create a 3D character controller with animations in Godot 4 using C#, Blender, and Mixamo. The tutorial begins by explaining how to set up a simple character model in Blender and animate it using Mixamo. It then shows how to import the model as a GLTF file in Godot, configure it, and implement a basic C# controller to use the animations in the game. The tutorial also covers how to set up the character's animator using an AnimationTree node and how to create conditional state transitions using advance conditions and expressions. Finally, the tutorial demonstrates how to implement the animated 3D character controller using a CharacterBody3D node and input actions.

Bullet points

  • The tutorial explains how to create a 3D character controller with animations in Godot 4 using C#, Blender, and Mixamo.
  • The tutorial begins by explaining how to set up a simple character model in Blender and animate it using Mixamo.
  • The tutorial then shows how to import the model as a GLTF file in Godot, configure it, and implement a basic C# controller to use the animations in the game.
  • The tutorial covers how to set up the character's animator using an AnimationTree node and how to create conditional state transitions using advance conditions and expressions.
  • The tutorial demonstrates how to implement the animated 3D character controller using a CharacterBody3D node and input actions.
  • The tutorial also provides tips on how to optimize the character's movement and animation transitions.
  • The tutorial concludes by providing a summary of the key concepts covered in the tutorial and encouraging readers to try out the techniques for themselves.

Setting up an animated 3D character controller (Godot 4/C#)

Let’s see how to animate a 3D character with Blender & Mixamo, and use it in Godot as a player avatar!

Video games are all about immersing yourself in another world and roaming about in some avatar body — and though this kind of 3D character that you pour your soul in can be tricky to create when you get into the real advanced stuff, making a basic character controller for your next Godot game is not that hard!

So in this tutorial, we’ll learn how to prepare a simple character model in Blender and animate it thanks to Adobe’s Mixamo tool. And then, we’ll see how to import this model as a GLTF file in Godot, configure it and its animations, and even implement a basic C# controller to use those animations in game.

As usual, since we’ll be coding our logic in C#, make sure that you have a version of Godot with .NET enabled.

Download a version of Godot with .NET support, to be able to program in C#! :)

And of course, don’t forget that you can get the demo scene and all the assets for this example on my Github 🚀 with all my other Godot tutorials :)

Also, the spaceships & icon assets are from Kenney’s library🚀

The tutorial is also available as a video — text version is below:

Now, with all that said, let’s dive in and discover how to import and setup an animated 3D character in Godot and C#!

Oh and by the way: if you’re still a bit new to Godot 4 and C# game dev, go ahead and check out my brand new “short-read” ebook: L’Almanach: Getting Started!

This quick, practical guide will have you explore the fundamentals, and it will teach you how to setup scenes, program in C#, design UIs, and even build your own tic-tac-toe game step-by-step :)

So if you wanna embark on your Godot 4/C# journey in less than 100 pages and for a low-price, don’t hesitate to have a look at the Gumroad page

Setting up our 3D character model

Alright, first of all, let’s hop in Blender to have a look at our 3D character for today.

Here, I’ll be using the basic blocky character from Kenney’s library, with the adventurer skin, that is a sort of Minecraft-like avatar, and that is already rigged for animation:

Typically, if we switch to Pose mode and translate or rotate a bone, we see that the body moves along with it… and so we could technically animate our character bit by bit like this:

But of course, this would be quite long… and also it would require that we have nice animator skills, which I personally don’t really ;)

So instead, a very cool solution is to use a great tool by Adobe, called Mixamo.

Mixamo is a free pose and animation library, that you can use to get a wide variety of animations on your models, and that is compatible with most game engines, and in particular with Godot.

By default, it will show animations on one of the built-in 3D characters:

But you can always import your own model, usually in the FBX format:

And it gets directly replaced in the preview on the right:

And now, you can use the search tools and browse the library to pick, check out and download any animation you want :)

For example, for this tutorial, I’ve downloaded a few typical moves for a 3D character: an idle pose, a running anim, a falling anim and even a punch movement.

I exported all of those from Mixamo, in the FBX format and with the skin option enabled:

For the run animation, I also made sure to tick the In Place option so that my rig just walks in place, because we’ll want our Godot object to move independently of the animation in the game engine:

Ok! And now, we just need to go back to Blender, delete our non-animated character and replace it with one of the new FBX files to get the character’s body with an animation:

If we start the Blender scene global timeline, you see the character now moves according to this idle pose we previewed and downloaded from Mixamo.

Now, to keep our character self-contained, and make it easier to use in a game engine like Godot, the best technique is to make sure we also re-import the other animations, and put them all on this reference armature object, as multiple actions.

So first, to know which actions we actually have on our character, let’s switch the bottom panel from the Timeline to the Dope Sheet:

Then, we’ll use the dropdown on the left to show the Action Editor:

You see that, for now, we have a single action with a pretty unreadable name — but which we can of course rename to the clearer “idle” name:

Now, to add our other Mixamo animations, we just need to import the other FBX files and you see that Blender automatically adds those actions to our first armature actions list:

So we just have to rename them as well, to make it easier to know which corresponds to what:

And when we’re done, we can select all of those other armatures that we copied animations from, and delete their hierarchies:

In the end, you see we have just a single copy of our little adventurer character, but with several animations that can all be played for this rig. The last step is to select this armature and all of its sub-hierarchy, so with all the children, and export it as a GLTF to use in Godot. In my case, since the scene also contains the default light and camera, I need to make sure to only export the selected.

And now, we can simply drag this file to our Godot project — and we’re all ready to configure and use our adventurer in our 3D scene!

Preparing the character’s animator

Ok — now that our character model is ready and imported, time to see how to actually use our animations in Godot :)

If we double-click on our GLTF model, we see that we indeed have our meshes, our skeleton with all of the bones we saw before in Blender, and at the bottom, an AnimationPlayer node with each and every one of our actions:

Actually, while we’re at it, let’s take this opportunity to tell Godot that some of those animations should loop. More specifically, the falling, the idle and the running animations should be set to the loop Linear mode with this dropdown on the right:

Now, we’ll simply re-import our model (with the Reimport button at the bottom of the popup panel), and we’re ready to get to it :)

So, a while ago, when we worked on our 2D character controller, we saw how, for 2D objects, we can use animated sprites to play animations. And more recently, in this other episode, we discussed how to use an AnimationPlayer node to move a 3D chest, rotate its lid, etc.

Now, as stated in Godot’s docs, the AnimationPlayer node is a very powerful tool that can adapt to a lot of situations… but not all. In particular, when you want to cleverly mix several pre-made animations together like these ones to animate a rig, you usually want to dive into another interesting tool: the AnimationTree.

An AnimationTree node doesn’t work on its own: it always needs an AnimationPlayer node to actually define and store the animation data. However, it is a handy way of mixing, sequencing or conditionally playing those animations depending on variables in your code. So, typically, to animate a 3D character, it’s a really interesting tool ;)

The usual technique when working with AnimationTree nodes is to create your overall 3D scene, slap your GLTF animated 3D model in it, and then add the AnimationTree node next to this model, under the root node:

Finally, to make the AnimationPlayer node inside our GLTF imported file accessible, we’ll right-click on our 3D model instance, toggle on its Editable Children property, and assign it in the Inspector of our AnimationTree:

Alright, now, we need to decide what exactly we want our AnimationTree to be able to do. If you take a look at the Tree Root dropdown, you’ll see that there are various ways to use an AnimationTree:

  1. You have a the basic animation node that simply allows you to reference and play a specific clip from your associated AnimationPlayer.
  2. You have blend nodes — those are a really cool way of mixing up several of your animations and controlling how much of it to apply to the skeleton of your character. Typically, by using this and telling Godot to mask the influence of the animations to just part of the rig, you could completely isolate the movement of the top of the body from the bottom, and have your avatar both run and aim at something.
  3. But here, we’ll actually go for the third option, that is fairly intuitive for a character controller and that will match our code logic pretty well: the StateMachine-based AnimationTree.

Now, if you’re not too familiar with the concept of state machines, I highly recommend you have a look at this previous tutorial in the series about FSMs.

Note: This episode was about using the FSM design pattern for code logic — but the base notions about states, transitions and just overall splitting your behaviour into more intuitive chunks apply the same to animation ;)

If we create a new State Machine root node as our AnimationTree node, we see that Godot auto-opens the Animation Tree panel at the bottom of the screen, that currently contains a Start and an End rectangle:

Those rectangle are the nodes in the tree, which here are also the states in our state machine. And what’s really cool is that if we right-click in this panel, we can add a new node and pick one of the animation clips on our associated AnimationPlayer node — typically, for now, let’s start with “idle”:

You’ll notice that, now that we’ve added this new state to our state machine, if we actually activate it in the Inspector of the AnimationTree node, over here, then our character can play this clip and show us an instant preview of what it looks like in the 3D viewport:

But of course, if we want this anim to play by default when the game starts, we’ll need to create a transition between the Start block, and this new idle block — which we can do by switching to the transition tool at the top of the panel, and then click-and-dragging from the Start rectangle to the idle one:

What this means is that now, when the game starts, Godot will automatically run this Start block and, in turn, this transition will lead our state machine to use the idle state as its default one. And, because this state is linked to the idle animation in our AnimationPlayer node, our character will default to this pose when the game runs.

Ok, that’s pretty cool but now, the real question is: how do we tell our state machine to run the other animations? And, in fact, how do we tell it to run those animations at the right time?

For example, if we were to add another state block for our run animation, and create a new transition between our idle state and this new state… the state machine would instantly switch to it!

So, how could we stop our machine from just skipping over the idle state and going straight to running? How could we tell it to wait for our character to be moving?

Creating conditional state transitions

Well, right now, our transition is too basic to stop from auto-advancing to the running state block.

Basically, when you create a transition, Godot will assume it is meant to be immediately traversed, and thus will directly update the current state (and linked animation) on your skeleton. Meaning that you don’t even see the first animation: if you click on the play icon of the Start block to see how this whole tree behaves, you’ll see the character instantly starts running.

To prevent this immediate trigger of a transition, we need to take a look at its Inspector, which we can access by using the select tool and clicking on the transition line between our idle and run states.

Now, there are two things we can do:

  • First, in the Switch section, we could change the Switch Mode of this transition to either Sync, or At End. Sync would basically trigger the change instantly too, but it would also make sure that we play the second animation from the same frame we interrupted our first one. That’s not really what we want here. At End would mean that Godot waits until it has played the idle animation entirely before switching to the run animation. Except that here… you might recall we made our idle animation loop, so it will never end! Plus it’s still not good: we want the idle state to stick until our avatar moves.
  • Which brings us to the second trick: using an Advance Condition or Expression.

If we open the Advance section, we see that it contains three properties:

  1. At the top, there’s a dropdown to decide whether our transition should be enabled or not. We can leave it to the default Auto value.
  2. Below, there’s an empty text field called Condition.
  3. And finally there’s a text area called Expression.

Those are the two most powerful ways of conditionally triggering a transition, as the names imply, and basically, they work as follows…

The Condition field lets you define a parameter for your animation tree, that you can then set from your code, and that should be a boolean. If it is true this frame, then the tree will trigger the transition; else, it will remain in its current state.

The Expression property is another technique: instead of defining and setting a parameter, you can also directly write an expression to evaluate in here, that will return a boolean, and that can use any of the properties exposed in the script attach to the Animation Expression Base Node object, defined inside the AnimationTree node, over here:

For example, suppose that we say that we want to switch from idle to running if the velocity of our character is not null. To do this:

  • We would first need to assign our root “Player” node as the Animation Expression Base Node:
  • Then reselect our transition and, in the Expression field, write our formula: velocity.length() > 0 (to check that the velocity vector is not null):

It is very important to remember that, as far as I’m aware at least, we can’t use C# in these expressions. So, just this once, we’ll be doing GDScript inside those little boxes — which is why you see I put length() in full lowercase, instead of the titled C# equivalent (Length()) :)

Of course, we could also create the reverse transition that takes us back from running to idle, if the velocity is null, with this other expression:

And now, finally, we need to actually create this velocity variable on our character!

So, let’s actually change the type of our root node from the basic Node3D to a CharacterBody3D and give it a capsule collider:

Note: As we discussed in this previous episode of the series, the CharacterBody nodes (either 2D or 3D) are Godot’s best tools for moving an object in your code using physics.

Then, we’ll create a new script on it, called Player:

Plus, you see here that Godot nicely setup a base CharacterBody3D template for us :)

So if we create the script and open it in the IDE, we see we already got quite some code to study!

using Godot;
using System;

public partial class Player : CharacterBody3D {
    public const float Speed = 5.0f;
    public const float JumpVelocity = 4.5f;

    // Get the gravity from the project settings to be synced with RigidBody nodes.
    public float gravity = ProjectSettings.GetSetting("physics/3d/default_gravity").AsSingle();

    public override void _PhysicsProcess(double delta) {
        Vector3 velocity = Velocity;

        // Add the gravity.
        if (!IsOnFloor())
            velocity.Y -= gravity * (float)delta;

        // Handle Jump.
        if (Input.IsActionJustPressed("ui_accept") && IsOnFloor())
            velocity.Y = JumpVelocity;

        // Get the input direction and handle the movement/deceleration.
        // As good practice, you should replace UI actions with custom gameplay actions.
        Vector2 inputDir = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
        Vector3 direction = (Transform.Basis * new Vector3(inputDir.X, 0, inputDir.Y)).Normalized();
        if (direction != Vector3.Zero) {
            velocity.X = direction.X * Speed;
            velocity.Z = direction.Z * Speed;
        } else {
            velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
            velocity.Z = Mathf.MoveToward(Velocity.Z, 0, Speed);
        }

        Velocity = velocity;
        MoveAndSlide();
    }
}

Now, if you’re not used to character movement, you can once again refer to what we discussed in the 2D character controller episode a while ago.

Basically, the idea is that each frame, when we process the physics in the _PhysicsProcess() built-in hook, we update our velocity depending on the user inputs, and then use it to move our object, if need be.

More specifically: if our character’s in the air, we apply the gravity so that it falls to the ground. Then, we check for a possible jump input to spring it upwards. And then, we use some built-in Godot input actions to move it horizontally. Finally, the MoveAndSlide() call at the end actually uses all this newly computed velocity to move the character for this frame.

This code is pretty nice and, if we run it, we see that we can already move forward, backwards, jump, and strafe left and right.

And the great news is that now, if we extract our velocity variable to the class itself and expose it (with the [Export] attribute), and then we get a reference to our AnimationTree node in the _Ready() function to make sure it is active when the scene starts:

public partial class Player : CharacterBody3D {
    // ...

    [Export] public Vector3 velocity = Velocity;

    public override void _Ready() {
        GetNode<AnimationTree>("AnimationTree").Active = true;
    }

    public override void _PhysicsProcess(double delta) {
        velocity = Velocity;

        // ...
    }
}

Then if we run the game… we automatically get our animations!

So alright — our character now stands still until we move it horizontally, then it starts running, and if we stop moving it goes back to idle. That’s pretty cool :)

But of course, the moves don’t really match all the animations, so there are a few more things to discuss now…

Implementing the animated 3D character controller

Alright, we have a character controller that can move forward or backwards, that can jump, and that can strafe. Now, strafing can be cool but, here, we don’t have any strafe animations and besides, I’d rather have our character rotate than glide on the side like this.

So in this final part, we’re going to edit Godot’s 3D character controller template to instead have the character rotate around the vertical axis, and to properly check for the ground in its state machine too, so that we get our falling animation when need be.

The first thing, replacing strafing with turning, is actually not that hard.

Basically, the idea is that, instead of using Godot’s GetVector() input converter that takes in 4 input directions and determines a matching 2D vector (which is a great way of getting a full horizontal movement and doesn’t really work here), we’re going to separate the X and Y input directions. This way, we’ll use the X direction for the rotation, and the Y rotation for the forward or backward movement.

To do this, we just have to use the GetAxis() input built-in to get both the turnStrength and the moveStrength:

public override void _PhysicsProcess(double delta) {
    // ...

    // Get the input direction and handle the movement/deceleration.
    // As good practice, you should replace UI actions with custom gameplay actions.
    float turnStrength = Input.GetAxis("ui_left", "ui_right");
    float moveStrength = Input.GetAxis("ui_up", "ui_down");

    Vector3 direction = (Transform.Basis * new Vector3(inputDir.X, 0, inputDir.Y)).Normalized();
    // ...
}

Then, in our direction vector computation, we just need to replace the inputDir variable with the moveStrength in the local forward vector (the Z coordinate), so that our vertical input axis only affects the forward and backward movement:

public override void _PhysicsProcess(double delta) {
    // ...

    // Get the input direction and handle the movement/deceleration.
    // As good practice, you should replace UI actions with custom gameplay actions.
    float turnStrength = Input.GetAxis("ui_left", "ui_right");
    float moveStrength = Input.GetAxis("ui_up", "ui_down");

    Vector3 direction = (Transform.Basis * new Vector3(0, 0, moveStrength)).Normalized();
    // ...
}

And of course, we can use the RotateY() built-in method to have our character turn left or right depending on the turnStrength input and some arbitrary rotation speed:

public partial class Player : CharacterBody3D {
    // ...

    public const float RotationVelocity = 3.5f;

    public override void _PhysicsProcess(double delta) {
        // ...
    
        // Get the input direction and handle the movement/deceleration.
        // As good practice, you should replace UI actions with custom gameplay actions.
        float turnStrength = Input.GetAxis("ui_left", "ui_right");
        float moveStrength = Input.GetAxis("ui_up", "ui_down");
    
        RotateY(-Mathf.DegToRad(turnStrength * RotationVelocity));
    
        Vector3 direction = (Transform.Basis * new Vector3(0, 0, moveStrength)).Normalized();
        // ...
    }
}

Also, it can be better to create our own input actions to replace Godot’s UI-related actions from the template. So let’s create various left/right/forward/backwards, jump and punch actions:

And then replace them in the script, like this:

public override void _PhysicsProcess(double delta) {
    Vector3 velocity = Velocity;

    // Add the gravity.
    if (!IsOnFloor())
        velocity.Y -= gravity * (float)delta;

    // Handle Jump.
    if (Input.IsActionJustPressed("jump") && IsOnFloor())
        velocity.Y = JumpVelocity;

    // Get the input direction and handle the movement/deceleration.
    // As good practice, you should replace UI actions with custom gameplay actions.
    float turnStrength = Input.GetAxis("left", "right");
    float moveStrength = Input.GetAxis("forward", "backwards");

    RotateY(-Mathf.DegToRad(turnStrength * RotationVelocity));

    Vector3 direction = (Transform.Basis * new Vector3(0, 0, moveStrength)).Normalized();    if (direction != Vector3.Zero)
    // ...
}

Ok, here we are! If we run our game again, you see that now, our character indeed turns when we press the left or right key, it moves forward or backwards when we press the up or down key, and the animations auto-update accordingly ;)

The next step is to ensure our character reacts to the ground disappearing from under its feet by switching to the falling animation.

So let’s start by adding it to our state machine:

Then, we’ll create a transition from the idle state to this new falling state and say that it should only be triggered if the avatar is not on the floor:

You see that the really cool thing with transition expressions is that, because we’re using a CharacterBody3D node for our source object, we can directly use the built-in is_on_floor() method, and it works directly! :)

(Again, don’t forget advance expressions need to be written in GDScript and not C#, which is why we’re using the snake case version here…)

Then, we can obviously also create the reverse transition in the case the character is indeed on the floor, and we should also check if its velocity is currently null:

Because if it isn’t, then, we should rather switch back to our character’s running state. So, we can finally complete this new round of transitions by adding links between the running state and the falling state. We’ll re-use the same check that our avatar is not on the floor in one direction, but in the other way we’ll need to check if the velocity is not null:

At this point, if we run our game again, you see that as soon as we jump and aren’t on the floor anymore, our character switches to the falling anim, and then it comes back to either the idle or the run animation, depending on whether or not we’re walking! Plus, the cool thing is that it also works if we’re just falling off a cliff, since it’s not tied to any jump state :)

As you can see, what’s really cool with this system is that we didn’t even need to update our code to add this new animation and its conditional transitions to our AnimationTree: as long as we’ve properly prepared our source node, typically by picking the right type, we can instantly setup some pretty advanced checks in just a few seconds.

Now, just to wrap up this tutorial, let’s quickly see an example of the other technique for doing conditional transitions: the Advance Conditions.

For example, let’s say we want to integrate our last action, the punch, into the state machine. The goal will be to check if we trigger our punch input action while on the ground and, in that case, interrupt the movement of our character and play the anim; then, when it’s done, we’ll go back to the idle state.

From the AnimationTree point of view, this is fairly easy to setup. We just need to add our new animation as a new state, create transitions from the idle and running state to it, and the reverse transition from punch to idle when the anim has ended.

For this, we can use the At End Switch Mode we saw earlier:

But for the transitions to the punch state, this time, using an expression would be a bit over-the-top.

Cause we’d basically need to create a boolean in our script, set it in our _PhysicsProcess() method if we trigger our action, and then use this in an expression that just proxies the value…

Which is a bit complicated for just a simple true/false flag. So, here, it would be easier to just define this flag as an Advance Condition, for example “punch”, and then set it in our script:

(Don’t forget to do it on both transitions: from the idle to the punch state block, and from the running to the punch state block!)

By the way, if you want to get a list of all the advance condition names you’ve defined on your AnimationTree node, you can always select it, go to its parameters section and, under conditions, check out the defined properties:

You can even toggle them on or off to test out some specific setups in the editor and see if they trigger the right sequence of animation states :)

But anyway, let’s handle the punch logic. If we’re on the floor and we just triggered our punch action, then we’ll set some temporary punched variable to true:

public override void _PhysicsProcess(double delta) {
    velocity = Velocity;
    bool punched = false;
    
    // Add the gravity.
    if (!IsOnFloor())
        velocity.Y -= gravity * (float)delta;
    else {
        // Handle Jump.
        if (Input.IsActionJustPressed("jump"))
            velocity.Y = JumpVelocity;

        // Handle Punch.
        if (Input.IsActionJustPressed("punch"))
            punched = true;
    }

    // ...
}

We’ll assign this temporary flag as the value for our condition parameter in the AnimationTree node by referencing this node, and setting its parameters/conditions/punch variable:

public partial class Player : CharacterBody3D {
    // ...

    private AnimationTree _anim;

    public override void _Ready() {
        _anim = GetNode<AnimationTree>("AnimationTree");
        _anim.Active = true;
    }

    public override void _PhysicsProcess(double delta) {
        velocity = Velocity;
        bool punched = false;
        
        // Add the gravity.
        if (!IsOnFloor())
            velocity.Y -= gravity * (float)delta;
        else {
            // Handle Jump.
            if (Input.IsActionJustPressed("jump"))
                velocity.Y = JumpVelocity;
    
            // Handle Punch.
            if (Input.IsActionJustPressed("punch"))
                punched = true;
            _anim.Set("parameters/conditions/punch", punched);
        }
        // ...
    }
}

Finally, to interrupt our movement while we’re punching, we can use a neat trick where we directly access the current state of our state machine, and here we’ll check to see if it’s “punch”.

We can get it by accessing the playback parameter of our AnimationTree node and checking out its current node. And in that case, we’ll reset the velocity of our character and skip the rest of the logic in the _PhysicsProcess() method:

public partial class Player : CharacterBody3D {
    // ...

    private AnimationTree _anim;
    private AnimationNodeStateMachinePlayback _animPlayback;

    public override void _Ready() {
        _anim = GetNode<AnimationTree>("AnimationTree");
        _animPlayback = (AnimationNodeStateMachinePlayback) _anim.Get("parameters/playback");
        _anim.Active = true;
    }

    public override void _PhysicsProcess(double delta) {
        velocity = Velocity;
        bool punched = false;
        
        // Add the gravity.
        if (!IsOnFloor())
            velocity.Y -= gravity * (float)delta;
        else { ... }

        // Punch? Interrupt movement.
        if (_animPlayback.GetCurrentNode() == "punch") {
            velocity = Vector3.Zero;
            Velocity = velocity;
            return;
        }
        // ...
    }
}

And that’s it! At this point, we’ve successfully implemented a basic 3D character controller that can move and rotate with user-defined input actions, and plays various animations to match these movements :)

Conclusion

So — here you go: you know how to setup a simple 3D avatar in Blender, animate it thanks to Mixamo and then use all of this in Godot to create a 3D player avatar with physics-based movement and anims!

If you enjoyed the tutorial, feel free to clap for the article and follow me to not miss the next ones — and of course, don’t hesitate to drop a comment with ideas of Godot tricks that you’d like to learn!

As always, thanks a lot for reading, and take care :)

To read more of my content, and articles from many other great writers from Medium, consider becoming a member! Your membership fee directly supports the writers you read.

Godot
Csharp
Programming
Tutorial
Game Development
Recommended from ReadMedium