Dana Vrajitoru
I355/C490/C590 3D Games Programming

I355/C490/C590 Lab 6 / Homework 6 Godot Version

Date: Wednesday, October 9, 2024. To be turned in by Wednesday, October 16. Here is a snapshot of the game:

In this lab, we will continue the project from the last week and add vegetation to the map, as well as the bee object.

Lab Part

Ex. 1. In this lab, we'll add first some trees to the map and then the bee object.

a. Vegetation Painting

Download the following file:

lab6.zip

Extract the files from it. In the project, create a new folder called trees and drag and drop both the .obj and .mtl versions for the birch and the small pine files to the trees folder.

Drag the file grass.png into the textures folder in your project. Then select the World object (the terrain) and click on the grass pain button above the scene. At the bottom, below the scene, click Add to add a new detail map.

In the Hierarchy on the left, you will notice that the World object now has a child of type HTerrainDetailLayer. Select this object. In the Inspector, drag the file grass.png and drop it over the Texture property of the object. Then below that, set the U Instance Scale property as 1 x 2 x 1. Choose a pretty large brush and go over the parts where you want it applied. Start from the position of the camera and going out from there. You may not see it in editing mode, but when you run the program, it should show up.

Using this tool to paint with the tree models seems possible but very slow. Instead, you can drag and drop these objects manually to the map from place to place, especially close to the camera's position. In theory, you can add another layer, add a tree to it as an Instance Mesh, then use it to paint. I only recommend this if your computer is powerful. We will add some trees through the code anyway.

Let's make the grass blend a little better with the terrain. Click on the Terrain menu above the scene, and select Bake Global Map. Then select the grass layer child of the terrain and in the inspector, under Shader Params, increase the values of U Globalmap Tint Bottom and top, as well as the U Bottom AO. Then play the game to see the effect. The grass should now blend with the terrain's color in that spot.

b. Bee Object

If you are happy with the result you got from lab4, open your file bee.blend in Blender. Delete the camera and the light, and then export it as a Wavefront (.obj) object. In the export dialog, check Colors under Geometry and Material Groups under Grouping. Make sure that the Materials are also exported. This should create the files bee.obj and bee.mtl.

If not, you can also use my solution to that lab that is included in the zip file.

Drag the file bee.obj into the Art section of the res:// folder. Click on the camera object in the hierarchy to select it and F to focus on it. Drag the bee.obj object from the resources to the scene around the camera and position it so that we can see it in the preview.

This object does not show the colors we've assigned to the vertices yet. Select the object and in the Inspector, expand Surface Material Override. This will show an array with as many elements as the number of individual objects you have in your bee in Blender. Next to the entry 0, click next to <empty> and assign to it a New StandardMaterial3D. Click on the sphere that appears to expand its properties, expand Vertex Color, and check Use as Albedo. You can click on the material and save it under a name like vertexColors. Then for each of the other array elements, you can click on the <empty> and then on Quick Load and select the material you created for the first one. You'll have to do this for the remaining elements of this array. After this, you should see the same colors that you had in Blender.

Add a CharacterBody3D object to the scene as child of the root and rename it Player. Drag the bee object over it to make it a child of the player. Write down the coordinates of the bee in the transform, then set them to 0. Zoom out to see where it is, then focus on it again.

Add a new script to the Player object called player.gd. Copy the variables at the top of the camera_3d script into the player script and delete the two pre-defined variables there. Copy all the functions from the camera script into the player script except for the function _process. Add a class variable speed_y initialized as 3 (you can tweak this one later).

In the player script, in the function _physics_process, replace the whole code with the following:

var input_dir := Vector3(0, 0, 0)
input_dir.x = Input.get_axis("ui_left", "ui_right")
input_dir.y = Input.get_axis("ui_page_up", "ui_page_down")
input_dir.z = Input.get_axis("ui_up", "ui_down")
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.z)).normalized()
if input_dir:
    velocity.x = direction.x * speed
    velocity.y = -speed_y * input_dir.y
    velocity.z = direction.z * speed
else:
    velocity.x = move_toward(velocity.x, 0, speed)
    velocity.y = move_toward(velocity.y, 0, speed)
    velocity.z = move_toward(velocity.z, 0, speed)

move_and_slide()

Then remove the script component from the camera and make the camera a child of the Player object. If you try it now, the bee and the camera should be moving pretty well, but not rotating yet with the mouse.

For the rotation, we're going to improve a little on homework 5. Add the class variables twist_angle and pitch_angle, both initialized as 0. Then replace the two lines inside the if statement in the function _unhandled_input with the following:

twist_angle += -event.relative.x * mouse_sensitivity
pitch_angle += -event.relative.y * mouse_sensitivity
print(pitch_angle)
if pitch_angle > deg_to_rad(180) and pitch_angle < deg_to_rad(300):
     pitch_angle = deg_to_rad(300)
elif pitch_angle < deg_to_rad(-60):
     pitch_angle = deg_to_rad(-60)
elif pitch_angle > deg_to_rad(60) and pitch_angle <= deg_to_rad(180):
     pitch_angle = deg_to_rad(60)

Now, add the following function to the class and call it from the function _physics_process just before move_and_slide:

func rotate_view():
    rotation = initial_rotation
    rotate_x(pitch_angle)
    rotate_y(twist_angle)
    forwardV = forwardV.rotated(Vector3(0, 1, 0), twist_input)
    rightV = rightV.rotated(Vector3(0, 1, 0), twist_input)

The difference is that we're not rotating the player little by little as we move the mouse, which has the effect that each new rotation pairs are applied to the result of the previous ones. We're accumulating the rotation angles in these two variables and then resetting the object's rotation and applying the collected rotations over x and over y just once.

Add another child of the player of type CollisionShape3D and set its shape as a capsule shape. Set its rotation by 90 degrees over y and z, or whatever it takes for it to align on the length with the bee's body. Then the radius and the height so that the body and head of the bee are more or less covered. It's ok if the legs, antennas, and wings are outside of it. Rename this collider as PlayerCollider.

Once they fit well together, click on the Player object and set its coordinates in the transform with the values you had before for the bee alone. Now the bee should be visible again in the camera preview.

d. Flower Object

Drag the files flower.obj and flower.mtl together from the zip file into the Art folder of your project. Then drag the .obj one to the the scene. Place it close to the bee so that you can compare them for size. I recommend using an orthogonal view to work on this part. For example, for the bee provided in the zip file, the flower should be scaled at least at 7 x 7 x 7.

The color for this object has been set at the level of the object and not of the vertices. By adding the .mtl file at the same time, the color should be imported properly. If it does not show up, then with the object selected, expand Surface Material Override in the inspector. Set the material for element 0 as a new standard material 3D. Click on it to set its properties. Expand Albedo and set the color to some dark green. This should be the stem. You can use the same color for the two leaves, or something different. Set the center of the flower in a light color like yellow and the petals in a darker color of your choice. Save the material for the petals in the textures folder under petal.tres (we'll be using it from the code later).

Add an object of type Area3D and name it FlowerArea. Make the flower object its child, then set its coordinates as 0 x 0 x 0. Refocus on the flower object. Add another child to the flower area of type CollisionShape3D and set its shape as a sphere. Name this collider FlowerCollider. Drag it up to align its center with the center of the flower (the pistil) and expand its radius to cover the entire ellipsoid in the center of the flower.

Add a new script to the flower area. Then click on the tab Node in the inspector and connect the signal body_entered with a function from the flower area script. This will be called when a body (the bee!) will collide with the flower. What we want to do is change the material assigned to the center of the flower so that the flower is marked as depleted for pollen.

Add a class variable called has_pollen and initialize it as true in the function _ready. Add the following class constant:

const PETAL = preload("res://textures/petal.tres")

Declare another class variable called flower_mesh and initialize it in the function _ready like this:

flower_mesh = find_child("Flower")

Also, make sure that the path written inside quotes is the correct path to the texture of the petals you have saved.

Then in the function _on_body_entered, check if this variable is still true, and if it is, set it to false. After that, set the material of the center of the flower to the material referenced above:

flower_mesh.set_surface_override_material(1, PETAL)

At this point, right-click on the flower area object and select Save Branch As Scene.

e. Code Instancing

Create a Node3D (generic) object to the scene called GM (for game master). This object will be in charge of objects generated in the code. Set its coordinates to 0 x 0 x 0 and add a new script to it.

Let's create a few instances of flowers in the function _ready and place them randomly on the map.

Let's start with creating a reference to the flower scene in the gm script. Grab the scene flower_area.tscn and drag it over the script. Just before you release it, press the Control (Cmd) button. It should create a constant like this:

const FLOWER_AREA = preload("res://Scenes/flower_area.tscn")

Redo the operation with the World object. The resulting code should look like this:

@onready var world: Node3D = $"../World"

but knowing how to do it for other references can be useful for the homework.

At the top of the class, define the following variables:

var flower_nr := 10
var flowers := []
var world_res = 0

In the function _ready, initialize the last variable as

world_res = world.get_data().get_resolution()

Then add the following function to the code:

func make_flowers():
    for i in flower_nr:
    var flower = FLOWER_AREA.instantiate()
    add_child(flower)
    var fx = randi_range(i, world_res)
    var fz = randi_range(i, world_res)
    var pos = Vector3(fx, 0, fz)
    pos.y = world.get_data().get_interpolated_height_at(pos)
    flower.position = pos
    flowers.append(flower)

and call this function inside the function _ready after the variable definition.

Test the code to see that some flowers are added to the scene. Drive the bee over the center of some of them to see if the color turns red. If it does not, double-click on the file flower_area.tscn. This will open the scene in a new tab. Connect the signal on_body_entered of the root of the subtree to the script attached to itself. Instead of creating a new function click the button Pick on the bottom right and select the function _on_body_enter that is already there.

f. Reset Button

We'll have to fix the reset functionality too. Disconnect the pressed signal of the button from the camera and connect it to the player script and the reset function. After that, also set pitch_angle and twist_angle to 0 in the function reset.

Homework Part

Ex. 1. a. Sound and Score

Add a sound to be played when the bee collides with a flower that still has pollen. Also add a score and increment it every time that happens. Add a text mesh to the screen to display the score, and update it when the score changes. Hint: you can add a reference to the player object to the flower_area script to be able to increment the score on collision:

@onready var player: CharacterBody3D = $"../Player"

b. Random Rotation

When the flower objects are instantiated in the code, apply a random rotation to them between 0 and 360 degrees around Y.

Ex. 2. Random Trees.

Use either one of the provided tree objects, or one of your own, to create a tree prefab scaled with the bees and flowers in mind, and use it in the code in gm.gd to generate a few random trees around the map. Make sure to place them at the appropriate height.

Ex. 3. Floating Bee

Declare a class variable in the class player.gd called hover_height and set its value to something like 4. Then in the function _ready, after moving and rotating the bee with keys and mouse, check the height of the bee is lower than the height of the terrain in that point plus the hover_height, and if it is, then adjust the y coordinate of the bee to the value of the sum. That way, if the bee gets too close to the ground, it will be pushed back up. This includes when we lower it with PageDown.

Homework Submission

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 6.