• Godot HelpProgramming
  • Is there a way to create different 2D movement modes and movement combinations (mouse + buttons)?

Hello there,

i want to create player charakter related controls ***in 2D*** where the player can move it's charakter by a combination of mouse and button input in the game world. The game prototype is an isometric game. I was following the tutorials of stefan_gamedev on youtube and my charakter can now be moved by clicking the left mouse button or alternatively by pressing the wasd keys to a certain destination. For the game i want to have two control modes for the charakter. The first mode shall be like in Ultima 8. In Ultima 8 one can control the Avatar by holding the left mouse button and the Avatar was running then in that direction the mouse pointer was pointing to. The second mode is intended as a combat mode. The player shall be able to view in any direction depending on the mouse location but at the same time the wasd buttons should be used for movement. So that a player can face it's enemies at the same time as he is moving backwards. So i was thinking about how i can do this but to be honest i don't have a clue. Maybe i should post the code first:

extends KinematicBody2D

onready var animation_tree = get_node("AnimationTree")
onready var animation_mode = animation_tree.get("parameters/playback")


var max_speed = 200
var speed = 0
var acceleration = 750
var moving = false
var destination = Vector2()
var movement = Vector2()
var move_direction = Vector2()


func _unhandled_input(event):
	if event.is_action_pressed("Click"):
		moving = true
		destination = get_global_mouse_position()
		print(position.direction_to(get_global_mouse_position()).normalized())


func _process(delta):
	pass


func _physics_process(delta):
#	ButtonMovementLoop(delta)
#	MouseMovementLoop(delta)
	get_input(delta)


func ButtonMovementLoop(delta):
	move_direction.x = int(Input.is_action_pressed("Right")) - int(Input.is_action_pressed("Left"))
	move_direction.y = (int(Input.is_action_pressed("Down")) - int(Input.is_action_pressed("Up"))) / float(2)
	if move_direction == Vector2(0, 0):
		speed = 0
		animation_mode.travel("Idle")
	else:
		speed += acceleration #* delta
		if speed > max_speed:
			speed = max_speed
		var movement = move_direction.normalized() * speed
		move_and_slide(movement)
		animation_tree.set("parameters/Walk/blend_position", movement.normalized())
		animation_tree.set("parameters/Idle/blend_position", movement.normalized())
		animation_mode.travel("Walk")


func get_input(delta):
	var velocity = Vector2.ZERO
	
	if Input.is_action_pressed("Right"):
		velocity.x += 1

	if Input.is_action_pressed("Left"):
		velocity.x -= 1

	if Input.is_action_pressed("Up"):
		velocity.y -= 0.5

	if Input.is_action_pressed ("Down"):
		velocity.y += 0.5

	if velocity == Vector2.ZERO:
		speed = 0
		animation_mode.travel("Idle")
	else:
		speed += acceleration #* delta
		if speed > max_speed:
			speed = max_speed
			velocity = velocity.normalized() * speed
			velocity = move_and_slide(velocity)
		animation_tree.set("parameters/Walk/blend_position", velocity.normalized())
		animation_tree.set("parameters/Idle/blend_position", velocity.normalized())
		animation_mode.travel("Walk")


func MouseMovementLoop(delta):
	if moving == false:
		speed = 0
	else:
		speed += acceleration #* delta
		if speed > max_speed:
			speed = max_speed
			movement = position.direction_to(destination) * speed
		if position.distance_to(destination) > 10:
			movement = move_and_slide(movement)
			animation_tree.set("parameters/Walk/blend_position", movement.normalized())
			animation_tree.set("parameters/Idle/blend_position", movement.normalized())
			animation_mode.travel("Walk")
		else:
			moving = false
			animation_mode.travel("Idle")


func MovementCombination(delta):
	pass

As you can see i was trying two different ways of button related movement. Both are working but it seems that the acceleration in the "get_input" function doesnt work like the one in "ButtonMovementLoop". Also it seems that the different functions are getting irritated by each other after i injected seem into the "physics_process" function. As an example: If i activate one button movement function (no matter if it's "ButtonMovementLoop" or "get_input") together with the "MouseMovementLoop" the movement of the player seems to be a lot slower as if i only use one movement function. But as soon as i disable delta in the "speed += acceleration" part of the code the movement is normal fast as one would suspect it to be. If i inject only one movement related function into the physics_process instead there is no problem with delta and the movement is as intended. Another problem is that if i press a button at the same time as my player charakter is moving to the assinged destination by mouse click the charakter is getting a small boost in movement speed.

Another thing would be that i don't really now how to get the player charakter to only look to the destination (the direction) of the mouse pointer. I was trying it with "look_at(get_global_mouse_position()" but then only one sprite is getting rotated instead of that the correct player sprite is shown for one of the eight achses.

In generell i am very unexperienced in terms of programming (bet you thought that already - lol) so i am thankful for any help. In the godot documentation i haven't found something which would help getting my movement idea done.

Oh and please excuse my maybe a bit clumsy english. I am not a native speaker.

This is a picture of my node tree:

Hi,

I would advice to separate movement logic from input and make it more general, then you can build more spohisticated functionalities on that common movement handling.

Creating some more general movement logic with more "abstract" input will give you ability to easly expand it and reuse in many other cases.

Here is some example I prepared:

  1. MovementController - general movement controller (abstract input is only move direction)
    class_name MovementController
    
    var max_speed = 10.0
    var acceleration = 5.0
    
    #Abstract input for movement
    var move_direction : Vector2 = Vector2.ZERO setget set_move_dir
    
    var controlledNode : KinematicBody2D
    var velocity : Vector2 = Vector2.ZERO
    
    func _init(controlledBody : KinematicBody2D, accel : float = 5.0, maxSpeed : float = 10.0):
    	controlledNode = controlledBody
    	acceleration = accel
    	max_speed = maxSpeed
    	
    func set_move_dir(dir : Vector2):
    	move_direction = dir.normalized()
    
    func process_movement(delta):
    	velocity = velocity.move_toward(move_direction * max_speed, acceleration * delta)
    	velocity = controlledNode.move_and_slide(velocity)
    1. Usage in KinematicBody2D to achieve keys + mouse movement:
      extends KinematicBody2D
      
      const EPSILON : float = 5.0
      
      onready var mvCon : MovementController = MovementController.new(self, 80.0, 100.0)
      
      var moveDestination : Vector2 = Vector2.ZERO
      var moveDir : Vector2 = Vector2.ZERO
      
      func _unhandled_input(event):
      	if(event is InputEventMouseButton):
      		if(event.button_index == BUTTON_LEFT && event.pressed):
      			moveDestination = get_global_mouse_position()
      			moveDir = moveDestination - global_position
      
      func dir_from_keys() -> Vector2:
      	var dir : Vector2 = Vector2.ZERO
      	dir.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
      	dir.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
      	return dir
      
      func close_enaugh(v1: Vector2, v2 : Vector2) -> bool:
      	return abs(v2.x - v1.x) <= EPSILON && abs(v2.y - v1.y) <= EPSILON
      
      func _physics_process(delta):
      	var keyDir = dir_from_keys()
      	if(keyDir != Vector2.ZERO):
      		mvCon.move_direction = keyDir
      		moveDir = Vector2.ZERO
      	else:
      		mvCon.move_direction = moveDir
      	mvCon.process_movement(delta)
      	
      	if(close_enaugh(global_position, moveDestination)):
      		moveDir = Vector2.ZERO
      There are also other things you mentioned in your question, but I don't have time today to explain everything, so maybe I will add something more tomorrow unless someone else won't give you needed answer.

Thank you Glyph! Can't wait to try this out tomorrow.

Hi,

So, there is some continuation.

First of all maybe some explanation. When you call move_and_slide() method it will move your KinematicBody2D, so having more than one method handling movement and using them the way you tried like for example:

func _physics_process(delta):
	ButtonMovementLoop(delta)
	MouseMovementLoop(delta)

Will actually cause your character move twice each time (because character will be moved by first method and later by second method again). Now as you noticed when you use any of button movement methods both with mouse movement method your character is moving a lot slower if you keep acceleration * delta part. Why is that if you move your character twice? That should make it move even faster. Yes, but that is because of this part of code in MouseMovementLoop() method:

if moving == false:
	speed = 0

That code will always set speed to 0 after each update (if you don't use mouse movement at that time). That mean your character don't have time to get its maximum speed. ButtonMovementLoop() will accelerate your character a bit each update, but MouseMovementLoop() keep changing speed to 0, so your character is always starting movement from beginning each update. When you removed delta multiplication, probably character was able to "speed up" to max speed during one update. Max speed you used is 200 and acceleration is 750, so what happened to speed each update was:

#speed is 0 at the beginning
(...)
speed += acceleration #Now speed is 750
if speed > max_speed:
	speed = max_speed #Now speed is 200
(...)
if moving == false:
	speed = 0 #Speed is again 0

Delta in most cases is very small because it is time elapsed between updates, so when you multiplied acceleration by delta, value was a lot smaller and from above explanation I hope you understand why character was moving slower (it never was able to achieve max speed if it was set back to 0 each time in MouseMovementLoop()).

Now if you used mouse movement both with button movement you wrote that player had some boost in direction you moved. That is because in that case MouseMovementLoop() doesn't set speed back to 0 each time and it caused double movement each time as I mentioned (not only double movement, but also double acceleration).

That is why it is better idea to keep only one code handling common things like movement (of course unless you don't need to have some very specific and dedicated movement for some object). That way you can use it for many different cases withuot need to write it many times. For example you can use MovementController I created for almost any input from which you are able to calculate desired movement direction. That may be mouse input or buttons or joystick or navigation path or anything. It is also better to have that common code shared because it will keep consistency in your game and in case you want to change something in movement calculations you need to change it only once and everything using it will be updated instantly. If you would try to have different movement calculations per input type then you could make mistake when changing some code and for instance moving by buttons would be different than moving by mouse.

Ok, now lets move to another topic which is something new - rotation. You tried to use look_at() method, but it doesn't work as you expected. That is because look_at() only handle calculation of rotation to make some object facing some point. That would be more useful in top down game with tanks for example. You could use that to lock tank canon on some object so it will always aim correctly in its direction. In case of game you are doing it won't handle rotations as you would like to.

As I understand you have some sets of images for your character for different directions (lets say 8 directions) and you want to display proper image for any direction player is aiming with mouse. I don't know if there is maybe some method that will handle that in Godot autamatically (because I haven't worked a lot with 2D games), but that is something I guess you need to handle yourself.

Here is some exaple how it may be done:

func isometric_rotation(angle : float) -> int:
	if(angle < 0):
		angle += 360
	return int((angle + 22.5) / 45.0) % 8

Above method will return values 0-7 depending on angle (in degrees). That values may be used as your direction to choose proper image to display. In that case it will return 0 for facing right and next numbers up to 7 for next positions counting angle clockwise.

Example how I used it to update sprite to make it "face" proper direction:

#Added that at the end of _physics_process method in KinematicBody2D
var lookDir = get_global_mouse_position() - global_position
$Sprite.frame_coords.x = (isometric_rotation(rad2deg(atan2(lookDir.y, lookDir.x))) + 6) % 8

I add 6 to value returned by isometric_rotation() because 6th frame in my sprite sheet is facing right, so for 0 returned by isometric_rotation() I need to use 6th frame.

Of course please notice that all these are only mathematical calculations. I assumed using atan2() which will return values like 0 to 180 and -180 to 0, not tested if it will work for every angle you will supply (especially probably larger negative angles won't work properly, because there is only angle += 360 and not handling angle wrapping).

I hope that carified you mistakes you did in your code and with that help you will be able to experiment and find solution that suits the best for your game.

@GlyphTheWolf said:

I hope that carified you mistakes you did in your code and with that help you will be able to experiment and find solution that suits the best for your game.

Thank you for your kind eplainations, Glyph. This helped me alot in understanding gdscript better and especially in terms how the _physics_process function works.

2 years later