00:00
00:00
3p0ch
If you like hard games try my Daxolissian System series

plasmid @3p0ch

Cat

Scientist

Read the manual & try stuff

So Cal

Joined on 2/13/10

Level:
13
Exp Points:
1,638 / 1,880
Exp Rank:
38,479
Vote Power:
5.48 votes
Audio Scouts
1
Rank:
Portal Security
Global Rank:
23,758
Blams:
50
Saves:
371
B/P Bonus:
8%
Whistle:
Normal
Medals:
4,611
Supporter:
3y 2m 21d

Godot tutorial - All the King's Men - Level scene and sword swipes

Posted by 3p0ch - September 8th, 2022


The level's node hierarchy

So far our player is just walking around on a gray background, so let's make a level to put him in. In Godot make a new folder of res://level and make a new scene (on the main menu bar: Scene – New Scene) with the root node being a YSort node. It will handle that sorting that we just talked about – its children will be drawn so anything with a smaller y-coordinate in its Position property (meaning that it's higher on the screen: remember that the top of the screen has y coordinate 0 and the bottom has a y coordinate of the number of pixels in the screen) will be drawn behind anything with a larger y-coordinate. Name that root node Level, and give it a child of a Sprite node and name that child Ground. The assets I made for the game can be downloaded from

https://www.newgrounds.com/dump/download/233a38b7d038de5619489482c8ff1ac3

Put the grass.png file in the res://level folder and then drag that to the Ground node's Texture property, and under Offset make sure that Centered is unchecked so you'll end up with the ground being at position (0, 0) and drawn covering the screen area. I made that 1024 x 600 pixels to cover the default screen size in Godot – if it's not covering the entire screen then from the main menu bar at the top of Godot go to Project – Project Settings – Display – Window and adjust the Width and Height to 1024 x 600. The Ground's position of (0, 0) works well with the YSort node because since the ground's position y-coordinate is 0 it will be drawn behind pretty much everything. Then add an instance of Player to the Level node: in the Godot file structure find res://player/player.tscn and click and drag it up to the Level node in the scene's hierarchy to make a Player instance in the scene. Then move the player so he's kind of in the center of the screen.


Lastly, we won't want the player to run off the edge of the screen so we'll add some collision objects that won't have sprites attached to them but will be there to keep him from running off the edge. Make a child StaticBody2D called LeftBorder, give it a child CollisionShape2D, and set the CollisionShape2D's Shape to be a RectangleShape2D with Extents of x: 10 and y: 300 (half the height of the screen, so the rectangle reaches 300 pixels up and down to cover the entire height). Then move the LeftBorder (StaticBody2D) node by setting its Node2D-Transform-Position to (0, 300) and you're done with the left border. Make the RightBorder similarly but at position (1024, 300), and make the BottomBorder with the RectangleShape2D using Extents of x: 512 (half the screen width) and y: 10 and position it at (512, 600). For the top border, remember how the player's hitbox is set up – we want that border to be a little thicker so if the player walks all they way up they won't have their head up above the top of the screen, so make its Extents be x: 512 and y: 40. You should be able to run that scene (save it as res://level/level.tscn if you haven't already) and have the player able to run around within the screen area. While we're at it, for now let's make the Level scene be the starting scene for the game (when you run an export or when you click the play button in the upper right that's not on a director's thingy) – in Project – Project Settings – General tab – Application – Run, set Main Scene to res://level/level.tscn.


Now for teaching points: the YSort node is a common way to handle 2D node sorting – deciding which nodes should be drawn in front of or behind others – for games with this sort of camera angle where you might have a guy walking around a tree like in the example above. But if you're making a different type of game then you should be aware there are different ways of handling layering if you don't use a YSort node. If you click on anything that inherits from Node2D (like the Ground or Player or the Borders) then in the inspector under Node2D you'll see a Z Index. Remember the Godot API reference? Check what the z-index does by looking at the Node2D entry (since z-index is in the Node2D group in the inspector so it's a property of Node2Ds) or click this link https://docs.godotengine.org/en/stable/classes/class_node2d.html#class-node2d-property-z-index Basically you can give the nodes numbers there so nodes with higher numbers will be drawn in front of nodes with lower numbers. If you're making a side-scroller then the background could have z-index 0, player and walls and enemies etc could have z-index 1, and any foreground effects like fog or such could have z-index 2. Also be aware that if two nodes have the same z-index then Godot uses the following rules: child nodes are drawn in front of their parent nodes (although you can change that by setting a node's Show Behind Parent property under CanvasItem to true), and the order in the hierarchy in the top left matters because "sibling" nodes will have the top node in the hierarchy list drawn furthest toward the back and nodes lower in the hierarchy list drawn in front.


The sword swipe

How should we handle swinging the sword? This is actually much more complicated than it seems. Stop and think about it for a minute before reading how I do it, and take a look at the game again and pay attention to how he's swinging the sword in the direction where the mouse is pointing.


If you did the "Your first 2D game" tutorial, then you might think of spawning the sword similar to how you spawn an enemy in that example – by making an instance of a scene. That's not how I did it. But since this is a tutorial and the purpose is to show you how stuff works in Godot, let's try that approach.


Approach 1: Instancing a scene for each swipe

Make a new scene, and your first question will be what type of node you want the sword swipe to be. KinematicBody? StaticBody? Area? Sprite? AnimatedSprite? In this case, we want to be able to detect collisions or overlaps with enemies, so a natural choice would be something that handles physics like KinematicBody / StaticBody / Area. And in my opinion, I just wanted to be able to detect if the sword overlaps an enemy that got hit but I didn't want to make Godot's physics engine forcibly push apart the sword node with whatever enemy got hit (I'll handle that myself from code because I know how I want to handle the pushback for both player and enemy) so I made it an Area2D node. If you want a detailed discussion of what the different nodes do and when you should pick which type of node, then it's at https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html But for now just make a new scene with a root node of Area2D named Swipe and give it child nodes of Sprite and CollisionShape2D. Drag swipe.png into the res://player folder and make that be the Texture for the Sprite. Remember that we scaled up the player by 2, so for the Sprite node under Node2D-Transform-Scale set x and y to 2. Then for the CollisionShape2D make a new CapsuleShape2D and edit it to overlap the sprite nicely. Save that as res://player/swipe.tscn. On the root node of the swipe scene, add a script with just the following code.

extends Area2D

var lifetime:float = 0.1

func _ready():
	connect("body_entered", self, "_on_body_entered")

func _physics_process(delta):
	lifetime -= delta
	if lifetime < 0.0:
		queue_free()

func _on_body_entered(body):
	print("body_entered signal, overlap with: ", body)

The print() will make Godot show things in the bottom center of the editor if you click on Output, in this case which body triggered the body_entered signal, and the rest of the code just makes the swipe get rid of itself after 0.1 seconds.


Now go to your player.tscn scene and open its code (either in the hierarchy clicking on the script icon next to the root Player node, or in the top center of the screen in Godot click on Script and then select the player.gd script that you saved earlier). Add a new line just above the speed variable declaration of

export var swipe_scene:PackedScene

and save the script. Since that line starts with "export", in the Inspector you should see a new category of Script Variables at the top that has Swipe Scene, which should look eerily similar to things like Node2D-Transform-Position. Drag swipe.tscn into that variable, and then add some more code to end up with this.

extends KinematicBody2D

export var swipe_scene:PackedScene
var speed:float = 400.0
var swipe_cooldown_max:float = 0.5
var player_to_swipe_distance:float = 26.0

var move_vector:Vector2
var swipe_instance:Area2D
var swipe_cooldown:float = -1.0
var last_mouse_position:Vector2 = Vector2.ZERO

onready var sprite_node = get_node("AnimatedSprite")

func _ready():
	pass # Replace with function body.

func _physics_process(delta):
	# Cooldowns
	if swipe_cooldown > 0.0:
		swipe_cooldown -= delta
	
	# Movement
	move_vector = speed * Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	if move_vector.length() > 0.1:
		sprite_node.animation = "run"
	else:
		sprite_node.animation = "idle"
	if move_vector.x > 0.01:
		sprite_node.flip_h = false
	if move_vector.x < -0.01:
		sprite_node.flip_h = true
	move_and_slide(move_vector)

func _unhandled_input(event):
	if event is InputEventMouseMotion:
		last_mouse_position = event.position
	if event is InputEventMouseButton:
		if (event.button_index == BUTTON_LEFT) and event.pressed and (swipe_cooldown <= 0.0):
			swipe_instance = swipe_scene.instance()
			add_child(swipe_instance)
			swipe_instance.look_at(last_mouse_position)
			swipe_instance.translate(player_to_swipe_distance * swipe_instance.global_transform.x)

We added variables swipe_cooldown_max and swipe_cooldown to handle a cooldown so you have to wait a tiny bit between swipes, and we made a swipe_instance variable that will be a reference to the swipe object that gets instanced. Remember that a scene like swipe_scene and an instance like swipe_instance are different types of things – a scene is like an abstract blueprint and only one swipe scene should exist in the game, while an instance or object is a more concrete thing that can show up in the scene tree and interact with stuff and potentially have multiple copies running around. The func _unhandled_input(event) function gets called any time the player does any sort of input, from pressing a key to moving or clicking the mouse to pressing a gamepad button, unless something else in the game "handles" the input first. The parameter named event is an object describing the event, so event might be a mouse motion or a mouse button press or a key press etc. In this case, the code is doing two things. First it checks to see if the input is a mouse motion, and if so it updates the variable last_mouse_position to be the coordinates on the screen where the mouse is. Why in blazes are we doing that? Because there isn't a command along the lines of current_mouse_position() that you can just call any time you feel like it. So the next best thing you can do is constantly check for mouse movement and if the mouse moves then store its position in a variable.


Second, it checks to the if the input is a mouse button press, and if so it checks to see if it's a left mouse button press (not a right mouse button press or the mouse wheel), whether the button was actually just pressed (because when you release the mouse button Godot also treats that as a mouse button event, but it has event.pressed set to false instead of true), and it makes sure that we're not still waiting on the cooldown for the next swipe. If all of those conditions are met, it makes an instance of the swipe and adds it to the scene tree as a child of the player, then makes it "look at" or point to (where the positive x-axis direction will point to the coordinate you indicate – that's why the sword swipe picture has the sword swinging right), and makes it move a little bit away from the player instead of sitting smack on top of the center of the player (which would normally happen with add_child). Taking a closer look at that line with swipe_instance.translate: it uses swipe_instance.global_transform.x. The global_transform of a Node2D is a description of how it's oriented, and the global_transform.x is a unit vector (meaning it has length 1) that tells you which way the Node2D's "right" is now pointing if you account for how it's rotated. In this case, since the swipe was rotated to point toward the mouse, swipe_instance.global_transform.x tells you which way the swipe_instance considers "right" (or "forward" since that's how the sprite was made) and nudges it in that direction multiplied by player_to_swipe_distance. If things are still unclear, try playing the game with and without that line commented out to keep it from being run (by putting a # character at the beginning of that line) to see what it's doing and why.


At this point, you might be asking yourself how you're supposed to remember all these input events and that you should check to see if an event is an InputEventMouseMotion or InputEventMouseButton and what properties like .pressed you should be checking for. Remember how I told you that you'll need to refer to the API a lot? Check InputEvent, and specifically InputEventWithModifiers (because keyboard and mouse input can have modifiers) and InputEventMouse and things that are inherited by it. Especially if you're thinking of making any games that might want to check for whether the player double-clicked on something without needing to write a lot of unnecessary code.


Try testing things out by playing the Level scene. It works like we want, sort of. The first thing you might notice is that if you move the mouse around the player then you can tell that the swipe is rotating around a pivot point at the player's feet (since we intentionally set things up to have the player's (0, 0) be there) instead of the center of the player which would seem more natural. That would be easy enough to fix by moving the swipe up a bit more, and I'll leave that as an exercise for the reader. Next look at the output window as you're playing to see what the swipe is colliding with. Sometimes it will collide with the player, and that's not really a problem because when we write the code to handle inflicting damage we can make the swipe not damage the player. And if you walk up to the StaticBody2Ds that you set up as borders then you'll see that you can swipe them and they'll be detected just as you would expect, so far so good.


Before moving on, I'll mention another approach you might want to take with your games. In this case the swipe sprite has the sword in it, and it makes sense for the swipe sprite to move around with the player if the player is moving, which is what will happen if the swipe is a child of the player. But what if you made the sprite be a swoosh of glowing crimson air that the sword passed through, and it would make more sense to have the swipe's sprite stay stationary instead of moving around with the player as the player moves before it disappears? In that case, you would want the swipe to be a child of whatever the root node for the scene is, instead of making it a child of the player (and therefore moving around with the player since child nodes move with their parent node). To do that, you could make these changes

			get_tree().current_scene.add_child(swipe_instance)
			swipe_instance.position = position

The get_tree() method gives you a reference to the scene tree, and its current_scene property is the root for the tree, so get_tree().current_scene.add_child() will add it to the root of the current scene tree. For the next line: on the right hand side if you just see a naked "position" without anything else then Godot will look for a position property of the object that the script is attached to, in this case the Player scene. The swipe_instance.position is of course the position of the swipe instance, so this will set the swipe_instance's position to be the same as the player's position (which will be nudged again in the next line). That's important to do in this case because when we did the get_tree().current_scene.add_child(swipe_instance), the swipe_instance was added as a child at the root scene's position (0, 0) which isn't where we want it since the player swiped his sword after all. We didn't worry about that earlier since when we added the swipe_instance as a child of the player it was added to the scene at the player's position.


Approach 2: Making the swipe a permanent child of the player

While that approach worked, it wasn't the approach I took in the game. Before we dive into it, try adding this line to the very end of your current player.gd script.

			print("Swipe instance overlapping bodies: ", swipe_instance.get_overlapping_bodies())

You'll notice that when you play, it doesn't work and doesn't jive with what you see from the body_entered signal. Hold that thought, and now we're going to wipe out the current swipe implementation and replace it with what I actually used in the game. In this case you're doing a tutorial, but when you're working on your own game if you ever feel like doing a major overhaul then be aware that there's not a great way to undo things if you decide it was a bad idea after all and you want your original design back. The simplest thing to do would be to make a .zip of your entire project's directory before you make any major changes so you'll have the option of scrapping your changes and going back to that .zipped version. Or use could use more advanced version control approaches described at https://docs.godotengine.org/en/stable/tutorials/best_practices/version_control_systems.html but I haven't used that approach myself.


We're going to make the swipe just be a child node within the Player scene, and no longer be its own Swipe scene. Open the Player scene and make a new child Node2D named SwipePivot. Then remember how you added the Player scene as a child in the Level scene by opening Level and dragging the player.tscn file to the root Level node in the hierarchy? Drag the swipe.tscn scene and drop it on the new SwipePivot node so it's a child of SwipePivot. Then right click the Swipe node in the hierarchy and click Make Local so it will now just be a child node without any sort of connection to the swipe.tscn scene. Detach the script from the Swipe node (by right clicking and picking remove script or by clicking the script with an X in the upper right of the scene hierarchy). Save the Player scene, and now you can delete the swipe.tscn scene and the swipe.gd script since everything will be done in the Player scene and by player.gd.


The SwipePivot node serves as a convenient way to make the sword pivot around the center of the player instead of around the player's feet when you move the mouse around while swiping. First, to see what happens if you don't make use of the SwipePivot node: try just going to the Swipe node and setting its Node2D-Transform-Position to (24, -8) which is kind of where you want it if the player is looking forward and swiping. Then play with the slider under Node2D-RotationDegrees, and it doesn't really do what you want since it just rotates the swipe around the Swipe node's position at (24, -8). Instead, set the SwipePivot's Node2D-Transform-Position to (0, -8) so its crosshair in the scene view is about on the player's shoulder, and set the Swipe node's position to (24, 0). Now you can use the SwipePivot's Node2D-RotationDegrees to rotate the swipe around like you want. Then in the hierarchy click the eye symbol to the right of SwipePivot so it and its children won't normally be visible – we'll make it appear and disappear from code in response to mouse clicks.


Next, edit the player's script to be this.

extends KinematicBody2D

var speed:float = 400.0
var swipe_cooldown_max:float = 0.3
var swipe_display_time:float = 0.05

var move_vector:Vector2
var swipe_cooldown:float = 0.0
var last_mouse_position:Vector2 = Vector2.ZERO

onready var swipe_pivot = get_node("SwipePivot")
onready var swipe_area = get_node("SwipePivot/Swipe")
onready var swipe_sprite = get_node("SwipePivot/Swipe/Sprite")
onready var sprite_node = get_node("AnimatedSprite")

func _ready():
	pass # Replace with function body.

func _physics_process(delta):
	# Cooldowns
	if swipe_cooldown > 0.0:
		swipe_cooldown -= delta
		if not swipe_cooldown > swipe_cooldown_max - swipe_display_time:
			swipe_pivot.visible = false
			sprite_node.rotation = 0.0
	
	# Movement
	move_vector = speed * Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	if move_vector.length() > 0.1:
		sprite_node.animation = "run"
	else:
		sprite_node.animation = "idle"
	if move_vector.x > 0.01:
		sprite_node.flip_h = false
	if move_vector.x < -0.01:
		sprite_node.flip_h = true
	move_and_slide(move_vector)
	
	# Point the sword toward the mouse
	swipe_pivot.look_at(last_mouse_position)

func _unhandled_input(event):
	if event is InputEventMouseMotion:
		last_mouse_position = event.position
	if event is InputEventMouseButton:
		if (event.button_index == BUTTON_LEFT) and event.pressed and (swipe_cooldown <= 0.0):
			swipe_cooldown = swipe_cooldown_max
			swipe_sprite.flip_v = not swipe_sprite.flip_v
			swipe_pivot.visible = true

Briefly, it now has a command at the end of _physics_process() that will point the SwipePivot node toward the last mouse position on every frame (whether it's visible or not) and _unhandled_input() will make it visible and set the swipe_cooldown in response to clicks. As a little bonus, it also flips the swipe's sprite vertically each time you swipe so he slashes in opposite directions on each swipe. Test it out, and it should pretty much act like you want. The only thing you might not like about it is that the player has to press and release the mouse button with each swipe of the sword, while I think it would be better if they could just keep the mouse button held down and continuously slash whenever the cooldown is up. You can do that by making a boolean variable to keep track of whether the mouse is pressed that will be updated whenever an InputEventMouseButton happens. Add it to the list of declared variables near the top

var move_vector:Vector2
var swipe_cooldown:float = 0.0
var last_mouse_position:Vector2 = Vector2.ZERO
var mouse_is_clicked:bool = false

Then make the _unhandled_input(event) function update mouse_is_clicked whenever the left mouse button is pressed or released

func _unhandled_input(event):
	if event is InputEventMouseMotion:
		last_mouse_position = event.position
	if event is InputEventMouseButton:
		if event.button_index == BUTTON_LEFT:
			if event.pressed:
				mouse_is_clicked = true
			else:
				mouse_is_clicked = false

And finally put the code to actually swipe at the end of _physics_process()

	# Point the sword toward the mouse
	swipe_pivot.look_at(last_mouse_position)
	
	# Swipe
	if mouse_is_clicked and swipe_cooldown <= 0.0:
		swipe_cooldown = swipe_cooldown_max
		swipe_sprite.flip_v = not swipe_sprite.flip_v
		swipe_pivot.visible = true

You can test that out and it should work nicely. Next add collision detection to the swipe with the get_overlapping_bodies() method like so

	# Swipe
	if mouse_is_clicked and swipe_cooldown <= 0.0:
		swipe_cooldown = swipe_cooldown_max
		swipe_sprite.flip_v = not swipe_sprite.flip_v
		swipe_pivot.visible = true
		for body in swipe_area.get_overlapping_bodies():
			print("The swipe overlaps with: ", body)

You should be able to see in the editor that the swipe overlaps the player and if you walk towards a border of the screen and swipe then it detects the screen's border. But why did it work now when it wasn't working when we tried to use get_overlapping_bodies() when we were instancing the swipe from a PackedScene? This is a common way people get tripped up with the physics engine. The physics stuff like determining what overlaps with what is just calculated once during the physics step that's run with physics_process, and if a node gets added to the scene then all that information won't be updated until the instanced node does its physics_process. It doesn't matter that we were checking for overlap from within the Player node's physics_process because the Swipe was instanced and overlaps were being checked immediately afterward; the swipe didn't have time to do its own physics before we started checking for collisions. But with this new approach the Swipe node is a permanent child of Player (whether it's visible or not) so it will have its physics be processed continuously and its collision data will be up to date when we check it. But be aware that similar problems with physics can crop up if you do things like make a huge change in a node's position and check for overlaps immediately afterward – the physics engine might not have updated the overlap data between the time you changed the position and the time you check for overlaps. Usually you'll want to use the move_and_slide or move_and_collide methods instead of just changing a physics object's Position property because they're really made for this sort of thing and checking for collisions as the object is in the process of moving.


So everything works like we want, and we can move on to making some enemies (the slimes and goblins, not our "allies" yet).


1

Comments

Very good tutorial so far. my mind was blown when you used Input.get_vector() instead of the usual if elses for movements, very smart. I'll continue the rest tomorrow.

This one tho needed a lot more pictures than reading.