Reputation: 361
I made a function that compares the speeds of two monsters the player and the enemy but I dont know how to make them do their attack and then go back to a state where the player can press a button again, at the moment the player attacks and then the enemy attacks without speed being a factor if I change the button to check_speed it becomes an endless loop
This is the code:
func check_speed(): # Gonna make it so speed becomes a factor
if P_team.monster_invetory[0].speed > enemy_scene.speed:
player_attack()
else:
enemy_attack()
func player_attack(): # Attacks the enemy
print("Player attacking")
enemy_scene.take_damage(P_team.monster_invetory[0].attack) # Calls the function to take damage
yield(get_tree().create_timer(2), "timeout") #Wait for 2 seconds
Enemy_battle_scene(enemy_scene) #This updates the display values
var is_dead = enemy_scene.take_damage(P_team.monster_invetory[0].attack) # Checks if dead
if is_dead:
current_state = GameState.WON
elif !is_dead:
current_state = GameState.ENEMYTURN
enemy_attack() # Here is where the enemy decides what to do, for now it only attacks
func enemy_attack(): # Attacks the player
print("Enemy attacking")
P_team.monster_invetory[0].take_damage(enemy_scene.attack) # Calls the function to take damage
yield(get_tree().create_timer(2), "timeout") #Wait for 2 seconds
Player_battle_scene(P_team.monster_invetory[0]) #This updates the display values
var is_dead = P_team.monster_invetory[0].take_damage(enemy_scene.attack) # Checks if dead
if is_dead:
current_state = GameState.LOST
elif !is_dead:
current_state = GameState.PLAYERTURN
#player_attack()
func _on_Attack_button_pressed():
if current_state != GameState.PLAYERTURN:
return
player_attack()
#check_speed()
Upvotes: 1
Views: 515
Reputation: 40295
A state machine by any other name.
You have states:
enum GameState {START, CHOICE, PLAYERTURN, ENEMYTURN, WON, LOST, CAPTURE}
var current_state = GameState.START
We can make that more useful by turning current_state
into a property and adding a "state_changed"
signal:
signal state_changed()
enum GameState {START, CHOICE, PLAYERTURN, ENEMYTURN,WON, LOST, CAPTURE}
var current_state setget set_current_state
func set_current_state(new_value):
current_state = new_value
emit_signal("state_changed")
Now, every time we set self.current_state
, it will trigger the signal. And of course we can connect to it.
So we can do this:
func _ready():
self.connect("state_changed", self, "_on_state_changed")
self.current_state = GameState.START
func _on_state_changed():
match current_state:
GameState.START:
_start()
GameState.CHOICE:
_choice()
GameState.PLAYERTURN:
_player_turn()
GameState.ENEMYTURN:
_enemy_turn()
GameState.LOST:
_lost()
GameState.CAPTURE:
_capture()
Why not put _on_state_changed
directly in set_current_state
? Because the signal is asynchronous. Also, you don't have to handle them all on this script. You may even have a script for each state.
Alright, let us start making those functions:
START:
func _start():
# initalization stuff
self.current_state = GameState.CHOICE
CHOICE:
func _choice():
# We have to wait for the button press, we do nothing here
pass
func _on_Attack_button_pressed():
if current_state != GameState.CHOICE:
return
if _player_speed() > _enemy_speed(): # Checks which monster is faster
self.current_state = GameState.PLAYERTURN
else:
self.current_state = GameState.ENEMYTURN
With some definition of _player_speed
and _enemy_speed
.
PLAYER_TURN and ENEMY_TURN:
var enemy_attk = false
var player_attk = false
func _player_turn():
yield(_player_attack(), "completed")
if _enemy_is_dead():
self.current_state = GameState.WON
else:
if enemy_attk:
player_attk = false
enemy_attk = false
self.current_state = GameState.CHOICE
else:
player_attk = true
self.current_state = GameState.ENEMYTURN
func _enemy_turn():
yield(_enemy_attack(), "completed")
if _player_is_dead():
self.current_state = GameState.LOST
else:
if player_attk:
player_attk = false
enemy_attk = false
self.current_state = GameState.CHOICE
else:
enemy_attk = true
self.current_state = GameState.PLAYERTURN
With some definition of _player_is_dead
, _enemy_is_dead
, _player_attack
and _enemy_attack
.
WON, LOST and CAPTURE:
func _lost():
# whatever happens
pass
func _won():
# whatever happens
pass
func _capture():
# whatever happens
pass
I have no idea.
Now, let us merge PLAYERTURN, ENEMYTURN into a new BATTLE state. Update the enum:
enum GameState {START, CHOICE, BATTLE, WON, LOST, CAPTURE}
Update the match statement:
func _on_state_changed():
match current_state:
GameState.START:
_start()
GameState.CHOICE:
_choice()
GameState.BATTLE:
_battle()
GameState.LOST:
_lost()
GameState.CAPTURE:
_capture()
When the button is pressed, we simply change to BATTLE:
func _on_Attack_button_pressed():
if current_state != GameState.CHOICE:
return
self.current_state = GameState.BATTLE
And now the main act:
func _battle():
if _player_speed() > _enemy_speed(): # Checks which monster is faster
yield(_player_attack(), "completed")
if _enemy_is_dead():
self.current_state = GameState.WON
else:
yield(_enemy_attack(), "completed")
if _player_is_dead():
self.current_state = GameState.LOST
else:
self.current_state = GameState.CHOICE
else:
yield(_enemy_attack(), "completed")
if _player_is_dead():
self.current_state = GameState.LOST
else:
yield(_player_attack(), "completed")
if _enemy_is_dead():
self.current_state = GameState.WON
else:
self.current_state = GameState.CHOICE
Let us talk about working with objects. First of all, have references to them. You already have enemy_scene
, let us use it. We need to add a counter part player_scene
.
onready var player_scene = P_team.monster_invetory[0]
By the way, if I were you, I'd add a script to EnemyPos
and PlayerPos
so I can do this:
$EnemyPos.update_view(enemy_scene)
$PlayerPos.update_view(player_scene)
Now, START can look like this:
func _start():
_update_view()
print("Battle started")
yield(get_tree().create_timer(0.2), "timeout")
self.current_state = GameState.CHOICE
func _update_view():
$PlayerPos.update(player_scene)
$EnemyPos.update(enemy_scene)
A suggestion: Add a "health_changed" signal to those scenes, and connect it to $PlayerPos
or $EnemyPos
respectively, so they can update automatically. In fact, we could use that signal to handle when the monsters die.
The attack is an issue, because these objects do not know their target. Will worry about that later.
Now update BATTLE to work with player_scene
and enemy_scene
:
func _battle():
if player_scene.speed > enemy_scene.speed:
yield(attack(player_scene, enemy_scene), "completed")
if enemy_scene.current_health == 0:
self.current_state = GameState.WON
else:
yield(attack(enemy_scene, player_scene), "completed")
if player_scene.current_health == 0:
self.current_state = GameState.LOST
else:
self.current_state = GameState.CHOICE
else:
yield(attack(enemy_scene, player_scene), "completed")
if player_scene.current_health == 0:
self.current_state = GameState.LOST
else:
yield(attack(player_scene, enemy_scene), "completed")
if enemy_scene.current_health == 0:
self.current_state = GameState.WON
else:
self.current_state = GameState.CHOICE
func attack(attacker, target):
print(attacker.name, " attacking")
target.take_damage(attacker.attack) # Calls the function to take damage
yield(get_tree().create_timer(2), "timeout") #Wait for 2 seconds
_update_view()
Please note that with a "health_changed" signal this code is much simpler. In that you won't have to handle winning and losing conditions on _battle
, nor call _update_view
from attack
.
Finally, we can write a version that puts them in an array:
func _battle():
var participants = [player_scene, enemy_scene]
participants.sort_custom(self, "check_speed")
for attacker in participants:
var target = _choose_target(attacker, participants)
yield(attack(attacker, target), "completed")
if player_scene.current_health == 0:
self.current_state = GameState.LOST
return
if enemy_scene.current_health == 0:
self.current_state = GameState.WON
return
self.current_state = GameState.CHOICE
func check_speed(a, b):
return a.speed > b.speed:
func _choose_target(attacker, participants):
for participant in participants:
if participant == attacker:
continue
return participant
Again, this would be simpler with a "health_changed" signal.
This would still require some modification to fully support more than two monsters:
You can, of course, keep the array outside of _battle
and remove participants when they die. Then checking if all enemy monsters are dead, is actually checking if all the remaining monsters are allies (and viceversa). Which you could do by keeping the count of how many ally or enemy monsters are there, and decrement it as they die. And you can do that connecting to the "health_changed" signal.
Ah, yes, a version that allows a monster to attack multiple times according to its speed. You will need a new attribute on these scenes. I'll call it exhaustion
, it should be 0
at the start of the battle.
func _battle():
var participants = [player_scene, enemy_scene]
var player_attacked = false
while (true):
var attacker = get_next(participants)
if player_scene == attacker:
if player_attacked:
# It is time for the player to attack again.
# Return control to the player.
self.current_state = GameState.CHOICE
return
else:
player_attacked = true
var target = _choose_target(attacker, participants)
yield(attack(attacker, target), "completed")
attacker.exhaustion += 1.0 / attacker.speed # <--
if player_scene.current_health == 0:
self.current_state = GameState.LOST
return
if enemy_scene.current_health == 0:
self.current_state = GameState.WON
return
func get_next(participants):
participants.sort_custom(self, "check_order")
return participants[0]
func check_order(a, b):
if a.exhaustion < b.exhaustion:
return true
if a.exhaustion == b.exhaustion:
return a.speed > b.speed:
return false
As you can see here, the rule is that the monster with less exhaustion
goes first. If two monsters have the same exhaustion
, the one with more speed
goes first. And every time a monster attacks, its exhaustion
is incremented. By how much? The inverse of speed
. Thus monsters with less speed
get exhausted quicker, and monsters with more speed
get exhausted slower. As a consequence, if a monster has more speed
than the other, it will end up doing multiple attacks for each one that the first one does.
Note: I called it "exhaustion" just so you have some intuition of how the value changes. However, it is important that this is not kept from battle to battle, it must be reset to 0
. If you imagine an exhausted monster vs a not exhausted one, the not exhausted one will get a lot of attacks in before the other one can do anything.
We need to keep track if the player attacked, as I mentioned in the original answer, to break the loop.
It depends on how exactly you want Speed to work, I'll only show the simplest approach. But first let us start at a base line.
Let us say, that disregarding speed, the player attacks, then the monster attacks, and then it waits for next input.
That means doing this something like this:
func _on_Attack_button_pressed():
# ...
player_attack()
enemy_attack()
Except those function yield
. So we are going to need this:
func _on_Attack_button_pressed():
# ...
yield(player_attack(), "completed")
yield(enemy_attack(), "completed")
The reason is that a function which yields returns a GDScriptFunctionState
.
Documentation on says GDScriptFunctionState
:
Calling @GDScript.yield within a function will cause that function to yield and return its current state as an object of this type
Here I'm using the "completed"
signal of GDScriptFunctionState
as suggested in the documentation on Coroutines & signals:
If you're unsure whether a function may yield or not, or whether it may yield multiple times, you can yield to the completed signal conditionally
Now, the simplest way to have "speed" work is to treat speed as turn order. Similar to D&D initiative. That means that all we do is sort those operations according to speed. So you do something like this:
func _on_Attack_button_pressed():
# ...
if player_goes_first():
yield(player_attack(), "completed")
yield(enemy_attack(), "completed")
else:
yield(enemy_attack(), "completed")
yield(player_attack(), "completed")
Giving your code, I'm guessing player_goes_first
will simply return P_team.monster_invetory[0].speed > enemy_scene.speed
. See Functions.
We could also write it like this:
func _on_Attack_button_pressed():
# ...
if player_speed() > enemy_speed():
yield(player_attack(), "completed")
yield(enemy_attack(), "completed")
else:
yield(enemy_attack(), "completed")
yield(player_attack(), "completed")
Where player_speed
and enemy_speed
are functions that return the appropriate values.
If you needed three participants, you can imagine the code (something like this).
However, your code would benefit from using objects with a speed
property and an attack
method (Those objects may or may not be nodes, see Classes).
If you have objects, it would be it easier to support more participants:
speed
, with sort_custom
.attack
on each one.Another approach is to have a get_next
function that returns the next object on which to call attack
. Then call get_next
on a loop. You would have a variable to keep track of whether or not the player has attacked (by checking if the what you got from get_next
is the player object), and stop before the loop player attacks twice (since the user is supposed to press the button to attack again).
Using a get_next
function would allow for more complex logic. For example, have a participant attack multiple times according to its speed
.
Upvotes: 1