adding week 5 detailed instructions for multiple levels, collectibles, bad guys and badguy movement
This commit is contained in:
parent
f1496a7424
commit
d509f3e4aa
BIN
graphics_assets/background/forest/block.png
Normal file
BIN
graphics_assets/background/forest/block.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 934 B |
249
week5/README.md
249
week5/README.md
@ -1,7 +1,252 @@
|
|||||||
This week we do:
|
This week we do:
|
||||||
|
|
||||||
* Screen Settings
|
* Level Loading, spawning scenes
|
||||||
* Collectible items
|
* Collectible items
|
||||||
* Enemies
|
* Enemies
|
||||||
* Player death
|
* Player death
|
||||||
* Spawning Scenes
|
|
||||||
|
### Moving the GameController
|
||||||
|
|
||||||
|
The game controller needs to be persistent across the entire life of the game. Godot provides a way to move a class outside of the display tree to live on its own in a persistent manner. It is called an 'autoload' and it can be found in project settings > globals.
|
||||||
|
|
||||||
|
When we have reassigned the game controller to an autoload, it can be accessed by the scene manager simply by class name - it, as well as any other code, will know how to find it.
|
||||||
|
|
||||||
|
Likewise the game controller should not directly reference any other class in the display list, since it is not longer a member of the display list. Instead, it should send out a signal that members of the display list can subscribe to.
|
||||||
|
|
||||||
|
### Example - Updating timer
|
||||||
|
|
||||||
|
Now that the game controller is persistent, it does not reload when the level timer completes. As a result, the level loads over and over again because it thinks the time has run out.
|
||||||
|
|
||||||
|
We can use this example to show the relationship of the scene manager to the game controller - because the scene manager DOES reload each time the level is updated. As it loads, it will tell the game controller to reset the timer.
|
||||||
|
|
||||||
|
In game controller:
|
||||||
|
|
||||||
|
```
|
||||||
|
func reset():
|
||||||
|
timeLimit = 10
|
||||||
|
```
|
||||||
|
|
||||||
|
And in the scene manager, we have a `buildLevel` function that gets called whenever the scene manager loads. This is a good place to tell the game controller to reset the timer.
|
||||||
|
|
||||||
|
```
|
||||||
|
func buildLevel()->void:
|
||||||
|
Gamecontroller.reset()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example - Destroy Signal (for crates)
|
||||||
|
|
||||||
|
Right now the scene manager counts up crates and tells the game controller how many there are.
|
||||||
|
|
||||||
|
Let's create a routine that allows the scene manager to connect signals from the triggers in our level to a function that will handle the logic.
|
||||||
|
|
||||||
|
Put all of your triggers in a blank Node2D, like we have done with our crates.
|
||||||
|
|
||||||
|
In Scenemanager>buildLevel:
|
||||||
|
|
||||||
|
```
|
||||||
|
for obj in triggers.get_children():
|
||||||
|
if obj is Trigger:
|
||||||
|
# wire up to GC
|
||||||
|
obj.triggerFired.connect(Gamecontroller._on_trigger_fired)
|
||||||
|
```
|
||||||
|
|
||||||
|
We can now update the game controller with a custom signal it will emit to indicate that the crate should be destroyed when it hits the "destroy" trigger.
|
||||||
|
|
||||||
|
In Gamecontroller:
|
||||||
|
|
||||||
|
```
|
||||||
|
func _on_trigger_fired(effect: Variant, body: Variant) -> void:
|
||||||
|
if effect=="destroy":
|
||||||
|
if body is Crate:
|
||||||
|
totalCrates -= 1
|
||||||
|
print("Crates Remaining " + str(totalCrates) )
|
||||||
|
destroySignal.emit(body)
|
||||||
|
if totalCrates == 0:
|
||||||
|
print("YOU WIN!")
|
||||||
|
get_tree().reload_current_scene()
|
||||||
|
```
|
||||||
|
|
||||||
|
Now if something hits the "destroy" trigger, the game controller checks to see if it is a crate, and if so signals to destroy it. This is heard by the scene manager, who deletes the crate.
|
||||||
|
|
||||||
|
### Example: Loading Scenes
|
||||||
|
|
||||||
|
Lets move the option to load scenes to the scene manager as well, and trigger it using a custom signal from the game controller.
|
||||||
|
|
||||||
|
In Scenemanager:
|
||||||
|
|
||||||
|
```
|
||||||
|
func loadLevel():
|
||||||
|
get_tree().reload_current_scene()
|
||||||
|
```
|
||||||
|
|
||||||
|
In Gamecontroller, change any references to loading scenes to simple signal emits:
|
||||||
|
|
||||||
|
```
|
||||||
|
func _on_trigger_fired(effect: Variant, body: Variant) -> void:
|
||||||
|
if effect=="destroy":
|
||||||
|
if body is Crate:
|
||||||
|
totalCrates -= 1
|
||||||
|
print("Crates Remaining " + str(totalCrates) )
|
||||||
|
destroySignal.emit(body)
|
||||||
|
if totalCrates == 0:
|
||||||
|
print("YOU WIN!")
|
||||||
|
levelCompleteSignal.emit()
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
func secondCounter():
|
||||||
|
timeLimit -=1
|
||||||
|
if timeLimit <= 0:
|
||||||
|
print("YOU LOSE")
|
||||||
|
levelCompleteSignal.emit()
|
||||||
|
```
|
||||||
|
|
||||||
|
We have now freed the game controller from the roll of loading scenes. And we have seen how to make additions to the functionality of our game.
|
||||||
|
|
||||||
|
### Multiple Levels
|
||||||
|
|
||||||
|
Copy your initial "game" level, and name the copies "level2" and "level3"
|
||||||
|
|
||||||
|
We can now create an array to keep track of each level.
|
||||||
|
|
||||||
|
```
|
||||||
|
var levels = ["res://scenes/game.tscn","res://scenes/level2.tscn","res://scenes/level3.tscn"]
|
||||||
|
var timers = [20,15,25]
|
||||||
|
var currentLevel = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the ready function of Gamecontroller to now use the timers array.
|
||||||
|
|
||||||
|
```
|
||||||
|
timeLimit = timers[currentLevel]
|
||||||
|
```
|
||||||
|
|
||||||
|
Likewise update the reset function:
|
||||||
|
|
||||||
|
```
|
||||||
|
func reset():
|
||||||
|
timeLimit = timers[currentLevel]
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, when the player loses we want to reload the current level. But if they win we want to load the next level.
|
||||||
|
|
||||||
|
Reload the current level by simply sending out a signal to loadLevel without updating the currentLevel.
|
||||||
|
|
||||||
|
```
|
||||||
|
func secondCounter():
|
||||||
|
timeLimit -=1
|
||||||
|
if timeLimit <= 0:
|
||||||
|
print("YOU LOSE")
|
||||||
|
levelCompleteSignal.emit(levels[currentLevel])
|
||||||
|
```
|
||||||
|
|
||||||
|
In SceneManager, update the loadLevel function to receive a string indicating what level to load. Note that we will do this using `call_deferred` which allows us to safely load a scene without interrupting currently running operations.
|
||||||
|
|
||||||
|
```
|
||||||
|
func loadLevel(level):
|
||||||
|
get_tree().call_deferred("change_scene_to_file",level)
|
||||||
|
```
|
||||||
|
|
||||||
|
When the player wins, we follow a similar pattern, but we increase the current level before emitting the signal. We also check to see if you just finished the final level, in which case you return to the beginning.
|
||||||
|
|
||||||
|
Update the `on_trigger_fired` function:
|
||||||
|
|
||||||
|
```
|
||||||
|
if totalCrates == 0:
|
||||||
|
print("YOU WIN!")
|
||||||
|
currentLevel +=1
|
||||||
|
#if on final level, go back to beginning
|
||||||
|
if currentLevel >= levels.size():
|
||||||
|
currentLevel=0
|
||||||
|
levelCompleteSignal.emit(levels[currentLevel])
|
||||||
|
```
|
||||||
|
|
||||||
|
Congratulations - you can now have as many levels as you wish! Try editing your additional levels to make them interesting.
|
||||||
|
|
||||||
|
-------
|
||||||
|
|
||||||
|
# Collectible Items
|
||||||
|
|
||||||
|
Now that we know how to add things to the game loop, let's test our skills by having collectible coins.
|
||||||
|
|
||||||
|
The process will be:
|
||||||
|
|
||||||
|
* Create a coin scene, and give it a custom signal when it is hit
|
||||||
|
* Place some coins in the level in a `coins` holder
|
||||||
|
* Have the scene manager loop through the coins wiring up their custom signal to the game controller
|
||||||
|
* Have the game controller track how many coins are collected, and emit a signal to destroy the coin when collected
|
||||||
|
|
||||||
|
The coin scene will have code that looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
class_name Coin extends Area2d
|
||||||
|
|
||||||
|
signal coinCollectedSignal(body, coin)
|
||||||
|
|
||||||
|
func _on_body_entered(body: Node2D) -> void:
|
||||||
|
coinCollectedSignal.emit(body, self)
|
||||||
|
```
|
||||||
|
|
||||||
|
Now in the Gamecontroller, receive that signal.
|
||||||
|
|
||||||
|
```
|
||||||
|
func _on_coin_collected(body, coin):
|
||||||
|
if body is Player:
|
||||||
|
destroySignal.emit(coin)
|
||||||
|
```
|
||||||
|
|
||||||
|
And in the SceneManager, you can update the destroy function if you like:
|
||||||
|
|
||||||
|
```
|
||||||
|
func destroy(body):
|
||||||
|
if body is Crate:
|
||||||
|
body.queue_free()
|
||||||
|
if body is Coin:
|
||||||
|
body.queue_free()
|
||||||
|
```
|
||||||
|
|
||||||
|
Don't forget to add a `coins` node to your additional levels, or they will trigger an error when the Scenemanager tries to find it.
|
||||||
|
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
# Bad Guys and Enemies
|
||||||
|
|
||||||
|
Adding a fun enemy to the level is remarkably similar to adding a collectible like a coin. The difference is that the enemy will be able to move around, and when we make contact with them the game controller will make a different decision - namely to hurt the player.
|
||||||
|
|
||||||
|
Just as with the coin, we will first create the bad guy. At a minimum, it will need a custom signal and a handler function for collisions.
|
||||||
|
|
||||||
|
Badguy script:
|
||||||
|
|
||||||
|
```
|
||||||
|
class_name Badguy extends Area2D
|
||||||
|
|
||||||
|
signal playerDamageSignal
|
||||||
|
|
||||||
|
func _on_body_entered(body: Node2D) -> void:
|
||||||
|
playerDamageSignal.emit(body, self)
|
||||||
|
```
|
||||||
|
|
||||||
|
As you did with the coins, you can make a node2D called "badguys" and add it to the game. You can then drag a Badguy scene into it, and place the badguy where you wish.
|
||||||
|
|
||||||
|
Now we can update the Scenemanager to have it wire up the badguys signals to the Gamecontroller.
|
||||||
|
|
||||||
|
In Scenemanager:
|
||||||
|
|
||||||
|
```
|
||||||
|
#wire up badguys
|
||||||
|
if badguys:
|
||||||
|
for obj in badguys.get_children():
|
||||||
|
if obj is Badguy:
|
||||||
|
obj.playerDamageSignal.connect(Gamecontroller.playerDamage)
|
||||||
|
```
|
||||||
|
|
||||||
|
The playerDamage function in the Gamecontroller can be very simple for now. We will revisit the player damage in the future. For the time being, it can look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
func playerDamage(body, badguy):
|
||||||
|
if body is Player:
|
||||||
|
print("player got damaged")
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user