Hello I want to recreate a game like “Kula Word” (PS1)
There is a ball that has 3 commands:
go forward 1 block
rotate 90 degrees left or right
jump forward 2 blocks
The ball moves on 2x2x2 cubes, with ortogonal movements (so it should be simple)
when the ball comes to an edge, the ball can wrap around that cube and the gravity changes
I am strugling to change gravity and rotation of the ball.
here is the script I’ve made…
extends CharacterBody3D
enum STATE {IDLE, MOVING, DEAD, WRAP}
@export var roll_speed = 0.5 # Secondi per completare un movimento
@export var jump_height = 4.0 # Altezza del salto
@export var grid_size = 2.0 # Dimensione di un blocco
@onready var ball: CSGSphere3D = $CSGSphere3D
@onready var ray_next_block: RayCast3D = $RayNextBlock
@onready var animation_player: AnimationPlayer = $AnimationPlayer
var state: STATE = STATE.IDLE
var initial_pos = .0
var target_pos = .0
var gravity_dir = Vector3.DOWN
func _physics_process(delta):
# Solo gravità
if not is_on_floor() and state not in [STATE.DEAD, STATE.WRAP]:
velocity += gravity_dir * 9.8
else:
velocity = Vector3.ZERO
if state != STATE.WRAP:
state = STATE.IDLE
move_and_slide()
if state != STATE.IDLE:
return
# Input movimento - SOLO UI_UP per avanzare di 1 blocco
if Input.is_action_just_pressed("ui_up"):
move_forward_one_block()
# Input salto - SOLO UI_ACCEPT per saltare di 2 blocchi
elif Input.is_action_just_pressed("ui_accept"):
jump_forward_two_blocks()
if Input.is_action_just_pressed("ui_left"):
dorotate(90)
elif Input.is_action_just_pressed("ui_right"):
dorotate(-90)
func dorotate(degrees: int):
if state != STATE.IDLE:
return
state = STATE.MOVING
var rotate_tween = create_tween()
var current_rotation = rotation_degrees.y
var target_rotation = current_rotation + degrees
#change this to rotate around transform up direction
rotate_tween.tween_property(self, "rotation_degrees:y", target_rotation, roll_speed)
rotate_tween.finished.connect(_on_move_finished)
func move_forward_one_block():
if state != STATE.IDLE:
return
state = STATE.MOVING
# Direzione avanti (basata sulla rotazione della palla o globale)
if not ray_next_block.is_colliding():
wrap_block()
return
var forward_direction = -transform.basis.z
var target_position = position + (forward_direction * grid_size)
# Animazione di rotolamento
var tween = create_tween()
tween.set_parallel()
tween.tween_property(self, "position", target_position, roll_speed)
animation_player.play("rotate")
tween.finished.connect(_on_move_finished)
func wrap_block():
var forward_direction = -transform.basis.z
var target_position = position + (forward_direction * grid_size)
var target2 = target_position + (Vector3.RIGHT.cross(forward_direction) * grid_size)
gravity_dir = -forward_direction.normalized()
var tween = create_tween()
tween.tween_property(self, "position", target_position, roll_speed)
tween.tween_property(self, "rotation:x", rotation.x + deg_to_rad(-90), roll_speed)
tween.tween_property(self, "position", target2, roll_speed)
tween.finished.connect(_on_move_finished)
#TODO fix to grid position?
func jump_forward_two_blocks():
if state != STATE.IDLE:
return
state = STATE.MOVING
var forward_direction = -transform.basis.z
target_pos = position + (forward_direction * grid_size * 2)
initial_pos = position
# Animazione di salto con arco parabolico
var tween = create_tween()
tween.set_parallel(true)
# Movimento orizzontale + arco verticale
tween.tween_method(_update_jump_position, 0.0, 1.0, roll_speed * 1.5)
animation_player.play("rotate")
#tween.finished.connect(_on_move_finished)
func _update_jump_position(t: float):
# Interpolazione lineare per il movimento orizzontale
var horizontal_pos = initial_pos.lerp(target_pos, t)
# Arco parabolico per il movimento verticale
var arc_height = jump_height * sin(t * PI)
position = Vector3(horizontal_pos.x, initial_pos.y + arc_height, horizontal_pos.z)
func _on_move_finished():
state = STATE.IDLE
func die():
state = STATE.DEAD
animation_player.play("die")
func goto_gameover():
get_tree().change_scene_to_file("res://levels/GameOver.tscn")
Struggling in what way? Don’t expect people to deduce that by reading the code and imagining how it would run. The more specific question you ask, the more likely you get some answers. Right now it just looks like you are expecting someone to figure out the code to make your game for you.
This is not a trivial mechanic you aimed at. Break it down and solve in smaller pieces.
Ok I need to be more clearer…
The main issue is that I want to rotate 90 degrees on the ball up vector (or gravity dir) always.
The issue is when the ball wraps around the cube edge, that means that the whole Player transform is rotated 90 degrees on the local X axis (vector right), with following code:
Ok I’ve found out that Quaternions may be the answer… but how I handle animation with tween?
extends CharacterBody3D
enum STATE {IDLE, MOVING, DEAD, WRAP}
@export var roll_speed = 0.5 # Secondi per completare un movimento
@export var jump_height = 4.0 # Altezza del salto
@export var grid_size = 2.0 # Dimensione di un blocco
@onready var ball: CSGSphere3D = $CSGSphere3D
@onready var ray_next_block: RayCast3D = $RayNextBlock
@onready var animation_player: AnimationPlayer = $AnimationPlayer
var state: STATE = STATE.IDLE
var initial_pos = .0
var target_pos = .0
var gravity_dir = Vector3.DOWN
var final_rotation: Quaternion
func _physics_process(delta):
# Solo gravità
if not is_on_floor() and state not in [STATE.DEAD, STATE.WRAP]:
velocity += gravity_dir * 9.8
else:
velocity = Vector3.ZERO
if state != STATE.WRAP:
state = STATE.IDLE
move_and_slide()
if state != STATE.IDLE:
return
if Input.is_action_just_pressed("ui_up"):
move_forward_one_block()
elif Input.is_action_just_pressed("ui_accept"):
jump_forward_two_blocks()
if Input.is_action_just_pressed("ui_left"):
dorotate(90)
elif Input.is_action_just_pressed("ui_right"):
dorotate(-90)
func dorotate(degrees: int):
if state != STATE.IDLE:
return
state = STATE.MOVING
quaternion = quaternion * Quaternion(Vector3.UP, deg_to_rad(degrees))
func move_forward_one_block():
if state != STATE.IDLE:
return
state = STATE.MOVING
# Direzione avanti (basata sulla rotazione della palla o globale)
if not ray_next_block.is_colliding():
wrap_block()
return
var forward_direction = -transform.basis.z
var target_position = position + (forward_direction * grid_size)
# Animazione di rotolamento
var tween = create_tween()
tween.set_parallel()
tween.tween_property(self, "position", target_position, roll_speed)
animation_player.play("rotate")
tween.finished.connect(_on_move_finished)
func wrap_block():
var forward_direction = -transform.basis.z
var target_position = position + (forward_direction * grid_size)
var target2 = target_position + (Vector3.RIGHT.cross(forward_direction) * grid_size)
gravity_dir = -forward_direction.normalized()
var tween = create_tween()
tween.tween_property(self, "position", target_position, roll_speed)
quaternion = quaternion * Quaternion(Vector3.RIGHT, deg_to_rad(-90))
#tween.tween_property(self, "rotation:x", rotation.x + deg_to_rad(-90), roll_speed)
tween.tween_property(self, "position", target2, roll_speed)
tween.finished.connect(_on_move_finished)
#TODO fix to grid position?
func jump_forward_two_blocks():
if state != STATE.IDLE:
return
state = STATE.MOVING
var forward_direction = -transform.basis.z
target_pos = position + (forward_direction * grid_size * 2)
initial_pos = position
# Animazione di salto con arco parabolico
var tween = create_tween()
tween.set_parallel(true)
# Movimento orizzontale + arco verticale
tween.tween_method(_update_jump_position, 0.0, 1.0, roll_speed * 1.5)
animation_player.play("rotate")
#tween.finished.connect(_on_move_finished)
func _update_jump_position(t: float):
# Interpolazione lineare per il movimento orizzontale
var horizontal_pos = initial_pos.lerp(target_pos, t)
# Arco parabolico per il movimento verticale
var arc_height = jump_height * sin(t * PI)
position = Vector3(horizontal_pos.x, initial_pos.y + arc_height, horizontal_pos.z)
func _on_move_finished():
state = STATE.IDLE
func die():
state = STATE.DEAD
animation_player.play("die")
func goto_gameover():
get_tree().change_scene_to_file("res://levels/GameOver.tscn")
and also wrap_block now seems not to work (because rotation is done instantly) but if I integrate quaternion rotation in tween it will be ok i guess
Again I’ve found solution with tween (thanks QwenAI)
but I have issue with jump and adjusting ball relative to the cubes (the ball must always be at the center of each cube face)
extends CharacterBody3D
enum STATE {IDLE, MOVING, DEAD, WRAP}
@export var roll_speed = 0.5 # Secondi per completare un movimento
@export var jump_height = 4.0 # Altezza del salto
@export var grid_size = 2.0 # Dimensione di un blocco
@onready var ball: CSGSphere3D = $CSGSphere3D
@onready var ray_next_block: RayCast3D = $RayNextBlock
@onready var animation_player: AnimationPlayer = $AnimationPlayer
var state: STATE = STATE.IDLE
var initial_pos = .0
var target_pos = .0
var gravity_dir = Vector3.DOWN
var final_rotation: Quaternion
func _physics_process(delta):
# Solo gravità
if not is_on_floor() and state not in [STATE.DEAD, STATE.WRAP]:
velocity += gravity_dir * 9.8
else:
velocity = Vector3.ZERO
if state != STATE.WRAP:
state = STATE.IDLE
move_and_slide()
if state != STATE.IDLE:
return
if Input.is_action_just_pressed("ui_up"):
move_forward_one_block()
elif Input.is_action_just_pressed("ui_accept"):
jump_forward_two_blocks()
if Input.is_action_just_pressed("ui_left"):
dorotate(90)
elif Input.is_action_just_pressed("ui_right"):
dorotate(-90)
func dorotate(degrees: int):
if state != STATE.IDLE:
return
state = STATE.MOVING
state = STATE.MOVING
var tween = create_tween()
tween.tween_property(self, "quaternion",
quaternion * Quaternion(Vector3.UP, deg_to_rad(degrees)), roll_speed * .5)
tween.finished.connect(_on_move_finished)
func move_forward_one_block():
if state != STATE.IDLE:
return
state = STATE.MOVING
# Direzione avanti (basata sulla rotazione della palla o globale)
if not ray_next_block.is_colliding():
wrap_block()
return
var forward_direction = -transform.basis.z
var target_position = position + (forward_direction * grid_size)
# Animazione di rotolamento
var tween = create_tween()
tween.set_parallel()
tween.tween_property(self, "position", target_position, roll_speed)
animation_player.play("rotate")
tween.finished.connect(_on_move_finished)
func wrap_block():
var forward_direction = -transform.basis.z
var target_position = position + (forward_direction * grid_size)
var target2 = target_position + (Vector3.RIGHT.cross(forward_direction) * grid_size)
gravity_dir = -forward_direction.normalized()
var tween = create_tween()
tween.tween_property(self, "position", target_position, roll_speed)
#quaternion = quaternion * Quaternion(Vector3.RIGHT, deg_to_rad(-90))
#tween.tween_property(self, "rotation:x", rotation.x + deg_to_rad(-90), roll_speed)
tween.tween_property(self, "quaternion",
quaternion * Quaternion(Vector3.RIGHT, deg_to_rad(-90)), roll_speed * .5)
tween.tween_property(self, "position", target2, roll_speed)
tween.finished.connect(_on_move_finished)
#TODO fix to grid position?
func jump_forward_two_blocks():
if state != STATE.IDLE:
return
state = STATE.MOVING
var forward_direction = -transform.basis.z
target_pos = position + (forward_direction * grid_size * 2)
initial_pos = position
# Animazione di salto con arco parabolico
var tween = create_tween()
tween.set_parallel(true)
# Movimento orizzontale + arco verticale
tween.tween_method(_update_jump_position, 0.0, 1.0, roll_speed * 1.5)
animation_player.play("rotate")
#tween.finished.connect(_on_move_finished)
func _update_jump_position(t: float):
# Interpolazione lineare per il movimento orizzontale
var horizontal_pos = initial_pos.lerp(target_pos, t)
# Arco parabolico per il movimento verticale
var arc_height = jump_height * sin(t * PI)
position = Vector3(horizontal_pos.x, initial_pos.y + arc_height, horizontal_pos.z)
func _on_move_finished():
state = STATE.IDLE
func die():
state = STATE.DEAD
animation_player.play("die")
func goto_gameover():
get_tree().change_scene_to_file("res://levels/GameOver.tscn")
I don’t know basis. Quaternions for me are Ok.
I’ve studied 3d geometry at university, I have a degree in IT
what are basis? (maybe I know, but I’m Italian so I may struggle with english)
Basis is a collection of non-colinear vectors that define a vector space, in such a way that any other vector in that space can be defined as a linear combination of basis vectors.
In very simple terms in the context of 3d graphics, basis is x, y and z axis vectors, those little red, green, and blue arrows you see in every 3d program.
It’s a fundamental concept in linear algebra. I find it very hard to believe you squeezed through an university degree in IT and never heard of it, especially in this day and age when “ai” is poured down our throats, as “ai” math is mostly high dimensional linear algebra.
When representing orientation, basis is a bit more intuitive than quaternions, as its numbers directly correspond to its visual representation.
Yeah I view it at university
I told you I am italian, I can speak english quite well, but may miss technical terms
also I’ve finished university 8 years ago
Omg now I have issues rolling around the block there is not a builtin function for this?
func wrap_block():
var forward_direction = -transform.basis.z
var target_position = position + (forward_direction * grid_size) # go to cube edge
var edgedir = Vector3.RIGHT.cross(forward_direction).normalized()
var target2 = target_position + edgedir * 4./3. # center on the other cube side center (aprox)
gravity_dir = -forward_direction.normalized()
var tween = create_tween()
tween.tween_property(self, "position", target_position, roll_speed)
tween.tween_property(self, "quaternion",
get_relrot(Vector3.RIGHT, -90) , roll_speed /2) # rotate player & camera
tween.tween_property(self, "position", target2, roll_speed)
tween.finished.connect(_on_move_finished)
I guess the issue is with the forward direction… the ball correctly rolls to the edge
but has issue to go straitght, some times, on the other edge
What? the ball can go upside down…
Global transforms How? I’m a newbie with Godot… any tutorial?
By the way I got a better result with, but still not working in some cases…
func wrap_block():
var forward_direction = -transform.basis.z
var target_position = position + (forward_direction * grid_size)
var edgedir = -transform.basis.x.cross(forward_direction).normalized() #Vector3.RIGHT.cross(forward_direction).normalized()
var target2 = target_position + edgedir * 4./3.
gravity_dir = -forward_direction.normalized()
var tween = create_tween()
tween.tween_property(self, "position", target_position, roll_speed)
tween.tween_property(self, "quaternion",
get_relrot(Vector3.RIGHT, -90) , roll_speed * .5)
tween.tween_property(self, "position", target2, roll_speed)
tween.finished.connect(_on_move_finished)
global_position instead of position, global_basis instead of basis etc… but it really depends on your node setup.
“not working” is not how IT engineers are supposed to talk. Describe the problem exactly if you need help with specific. Post your scene structure, illustrate the unwanted behavior with videos, show examples/mockups of desired behavior…
I have zero interest in tutorials so I don’t know any. The internet is full of them anyway, so search.
I don’t think you need to rotate the player. In the game you referenced rotating the level only serves to change its layout around the player - and nothing else. At any point in game, the player can only move left-right-forward-back, and fall down. Changing the literal up direction in real time grants you nothing but confusion.
I don’t think you need to force-center the player in a cell either. Notice that the game logic only allows the player to stay in a cell or move from one cell to another. Implement the game logic to do that. Define an array of cubic cells, track which cell the player is in, whether a cell below is solid, when he moved to a cell that wraps around a corner etc. Once you have that internal logical state figured out you can visualize it with arbitrary models and animations - but that is a separate concern. Having it all lumped together makes things overwhelming and confusing.