In this homework we will continue Lab 8 to implement the game Key Master.
This game will be composed of 5 levels. In each level, the player moves through a maze and must open a door. In order to open a door, the player must acquire first a key. Both the key and the door's positions are randomized in the maze. An opponent NPC will also chase the player and if they catch them, the level is lost and can be restarted. In Level 1, we start with a large maze, and in each new level, it shrinks down to make it more challenging to escape the opponent.
Note. I have added .blend versions of the objects we use in this program to Canvas - Files - Week 10, in case you want to make modifications.
Create another scene called end, similar to start but displaying the keyWon image instead. The caption on the button should say "New Game". The action can be the same as for the first scene. Add a text mesh (any kind) object displaying the credits.
If you have not set the window size in the lab, open Project Settings from the Project menu and set the window size as 1200 x 700, or something smaller if it doesn't fit your screen. Make sure that the application is Windowed. If you've already set these, you don't need to redo this now. Arrange the sprites in the start and end scene so that they cover the entire viewport.
a. Player Orientation
Add a feature to the player.gd class such that when the player moves to the left and right, the creature is rotated that way, and when it moves backwards, it faces the screen. The easiest way to do this is to add a reference to the child of the Player object at the top of the class:
@onready var raptor_walk: Node3D = $raptor_walk
Then in the function _physics_process, just before the call to move_and_slide, if velocity.x > 0, you can set the rotation of this object to 90 over y:
raptor_walk.rotation = Vector3(0, 90, 0)
Then use the values 0, 180, and -90 for the rotation over y as appropriate for the other cases. By rotating the child of the player and not the player itself, we'll have the player move in the right direction even when it's rotated.
b. Key and Door
Add a node of type StaticBody3D called KeyBody and make the key its child. Set the coordinates of the key to 0 x 0 x 0. Add a second child to this node of type CollisionShape3D with a box collider as the shape, and edit the box to cover the key well. Redo the operation for the door.
Add references to the key and door body objects in the class gm.gd.
Declare a function void init_level(). First, move the whole code from the function _ready into this new function, and make a call to it in the function _ready.
In init_level, check the value of Global.level and for level 1, initialize maze_width and maze_height with 30 both. For each next level remove 5 from these sizes. Then call the function make_maze with the values of these variables.
Add another function to the class called random_space(). This function should generate a random integer r between 0 and maze_width and c between 0 and maze_height, and check if the cell in maze at that position is a space. Repeat the procedure until you find a space. Then return Vector2i(r, c) from the function.
Going back to the function init_level, call the function above to generate a space position for the key and for the door. Use the functions col2X and row2Z to get world coordinates for these objects, then apply them to X and Z in their transform. You may have to add or subtract half of the size over X of the brick prefab from these coordinates. This should place the key and door properly in the maze.
c. Collision
Add a class variable to player.gd called has_key initialized as false in the function _ready.
Add the following function to the script player.gd
func collision_action(node): print(node.name)
Then in _physics_process, add the following code just before move_and_collide:
var collision_info = move_and_collide(velocity * delta, true) if collision_info: collision_action(collision_info.get_collider())
Modify the function collision_action so that if the node.name.contains("Key") returns true has has_key is false, you set has_key as true. We want to remove the key shape from its current body and attach it to the raptor's body. After doing that, we also want to discard the empty shell that contained the key, otherwise the collider will prevent the raptor from moving through. Add the following code to do that:
var the_key = node.find_child("Key") node.remove_child(the_key) the_key.scale = Vector3(0.1, 0.1, 0.1) the_key.position.y += 0.3 add_child(the_key) node.queue_free()
In the same collision function, add another test for the name of the node containing "Door". If the player collides with the door, check if the flag has_key is true. If it is, then play a cheerful sound and wait for a short delay. After the delay, increase the level number, and if it's less than or equal to 5, then reload the scene. If not, then load the end scene.
To implement a short delay, declare a boolean class variable end_level initialized as false in _ready, and another class variable count_down initialized as 0.0. When a game end condition occurs, set end_level to true and count_down to the number of seconds you want.
Add a function _process to the player where you check if end_level is true, and if it is, then subtract delta from count_down. If the count_down is <= 0, then reload the current scene or switch to the end scene based on the level number, as described before.
Add a text box (label) displaying the level number. When the game is won or lost, make it display that information.
Add an opponent creature to the scene. You can use the following or your own:
Make this object a child of a CharacterBody3D object called Enemy and add a collision shape sibling with an appropriate shape. Add a reference to it in the gm.gd. Randomize its starting position as you did for the key and door.
Add a case to the player's collision function where if the name is "Enemy", you display the message that the game is lost, play some sad sound, and restart the scene without changing the level number after some delay.
Add a function in gm.gd to have the enemy object try to follow the player. You can set its velocity over x and z to move towards the player. Like, if the X coordinate of the player is less than the X of the enemy, set its x velocity with a negative value, otherwise with a positive value. The same for z. Call this function from _physics_update.
You can try to make it more clever by using the information in the maze. Like, if its velocity.x is negative but the maze at its position -1 over the column is not a space, set velocity.x to 0. The same for z. Use the functions x2Col and z2row to map its current coordinates to the maze array.
Create a Windows executable like you did for Homework 5. Create a zip of the build folder, and add the script files to it. Also take a screenshot of the running game and add it to the zip file. Make sure that the zip file contains:
Submit the zip file containing the Windows executable and the script file to Canvas, Assignments - Homework 8.