Categories
Advanced Tutorials

Basic Coding #6: Make A Highly Dangerous Soccer Game

Preface

Crayta allows you to create any game you can imagine. Sometimes thinking of a game completely from scratch can be a bit daunting, but what about taking a game that already exists and turning it on its head? Enter, highly dangerous soccer – a soccer game which involves guns, traps and the odd exploding barrel.

Introduction

This tutorial will guide you through taking a Team Deathmatch level template and turning it into a soccer game using some Lua scripting in Crayta’s Advanced Editor

This tutorial is aimed at Crayta players who are comfortable with most of the tools in the Advanced Editor, but assumes no prior coding experience. If this is the first time you have opened the Advanced Editor then you may want to go through the Advanced Workflow Tutorial and the Learn To Code With Crayta tutorials to familiarise yourself with the interface. 

In this tutorial you will explore:

  • Changing the Team Deathmatch scoring and game mechanic using Lua Scripting
  • How to “kick” a football
  • Adding custom danger and explosives to your football pitch

The Beautiful Game

This isn’t going to be a typical football game. This is going to be a Crayta style soccer game. Firstly, let’s simplify the rules to give us a game where:

  1. Two teams of players kick a ball around a pitch
  2. The object of the game is to put the ball in the opposing team’s goal

Solving these problems is fairly straightforward.

Players can already kick a ball around the world (using the inbuilt physics engine) so you don’t need to code this yourself.

The Team Deathmatch game mode already has team management and scoring functionality to manage the team assignment and some of the scoring so you don’t need to code this yourself either!

This leaves the goal scoring mechanic and any level based aspects to code. Time to dive in.

Creating The Game

Start by creating a new Team Deathmatch game. Add a Ball (Football Jumbo) Prop to the game.

Use the Entity Editor to change its name to “football” and check the physicsEnabled checkbox.

Close the Entity Editor. Build a goal on your map using Voxel tools. This goal is going to be tweaked and updated when you come to test and balance your game, so don’t spend too long here. 

Ball Respawn

When the ball is kicked into the goal it needs to allow a short time for player celebrations and then respawn at the center point.

Add a Trigger to the World. Move the Trigger to the goal and increase its size until it fills the whole goal. Name the Trigger goalTeam1.

You will use the Trigger to call a function in the gameScript when the ball enters the goal. 

Add a Locator to the center of the game. You will use this Locator as the spawn point for the ball. Name the Locator footballSpawnPoint.

Click on the gameController in the Entity Explorer and open the Code Editor for the gameScript.

This Script contains a lot of the logic for the game, including the scoring when a Player kills another Player (as this is the game logic for a Team Deathmatch game) and assigning Players to teams. You can look through the code to see how the game works. In this guide you will be repurposing this logic to work the way you want it to. 

The gameScript needs a reference to the footballSpawnPoint so that it can respawn the ball at this location. Add a new Property to the Property Bag with the name “footballSpawn” of type “entity”.

{ name = “footballSpawn”, type = “entity” }

Use the Entity Explorer / Editor on the right hand side to drag the footballSpawnPoint to the footballSpawn Property on to the gameController / gameScript Property Bag.

To handle the logic when the ball enters the goal we will now write a function which is called by the Trigger from the OnTriggerEnter Event

Add a function after the Property Bag called GoalScored.

function GameScript:GoalScored(other, theTrigger)

end

The function contains two arguments:

  1. other – this is a reference to the Entity which has entered the Trigger.
  2. theTrigger – this is a reference to the Trigger itself. We will use the same function for both goals, so we will need to use this reference to figure out which team has scored.

To respawn the ball the following steps should occur:

Fill out the function so it contains the following:

function GameScript:GoalScored(other, theTrigger)
	
	-- Check if the thing which entered the goal Trigger (other) was called football
	if other:GetName() == “football” then

		-- Set ball position to ballSpawn position
		other:SetPosition(self.properties.footballSpawn:GetPosition())

		-- Halt ball velocity and spin
		other:SetVelocity(Vector.Zero)
		other:SetAngularVelocity(Rotation.Zero)
	end
end 

Use the Entity Explorer to call the GameScript:GoalScored function from the goalTeam1 Trigger OnTriggerEnter Event. 

Select the goalTeam1 Trigger in the Entity Explorer. Scroll to the onTriggerEnter dropdown box. Select the following:

  • Entity: gameController
  • Script: gameScript
  • Event: GoalScored

Test the game and check that when you score a goal with the ball, the ball is respawned at the footballSpawnPoint Locator position.

Adding Time For Celebrations

It’s not scoring a goal unless there is a little bit of time set aside for gloating. You can add a delay before the ball is respawned to the spawnBallLocator by using the inbuilt scheduler.

Tech Tip: Wait Off The Main Thread

The Wait() function in Crayta will stop the next line of code from running until after a certain amount of time, which can be very useful. However, this could be dangerous because it could cause the whole game to freeze whilst it waits for Wait() to complete. To avoid this, the Crayta developers have made sure this can’t happen – if you try to use Wait() on its own it just won’t do anything.

-- some code here
Wait(5.0) -- this Wait() will be ignored
-- some more code here

To use Wait() you will need to use self:Schedule(). This function moves the code within the brackets away from the main thread, so the game won’t lock up. This means you can now use the Wait() function. For example:

self:Schedule(
function()
	Wait(5.0) -- wait for 5 seconds before moving to the next line
	-- Here is some code that is executed after the wait function
end
)

Change your GoalScored function to reflect the following:

function GameScript:GoalScored(other, theTrigger)
	
	-- Check if the thing which entered the goal Trigger (other) was called football
	if other:GetName() == “football” then
		
		-- Use the scheduler
		self:Schedule(
			function()
				-- wait for 3 seconds
				Wait(3.0)

		-- Set ball position to ballSpawn position
		other:SetPosition(self.properties.footballSpawn:GetPosition())

		-- Halt ball velocity and spin
		other:SetVelocity(Vector.Zero)
		other:SetAngularVelocity(Rotation.Zero)

	end -- end the code called within the scheduler
)

	end -- end ball name check

end -- end GoalScored function

Test your changes to make sure the code still works, and that you have time to celebrate (feel free to tweak the length of the Wait()).

Adding The Score

The Team Deathmatch game template has some scoring logic already implemented. To score a point in Team Deathmatch you just need to kill another player. However, for the football game you want to score a point when the ball enters the goal, and not when a Player is killed. To do this you combine the goal scoring functionality you’ve already written with the scoring mechanic that is prebuilt.

If you scroll to the bottom of the gameScript you will see two functions:

  • AddTeamScore(team, num) – this function adds an amount to the team score. In the brackets; 
    • “team” refers to the team number (1 or 2) to add score to, and 
    • “num” refers to the number of points to add to their score (defaults to 1)
  • OnUserDied(userEntity, fromEntity, selfMsg, otherMsg, youKilledMsg) – this function is called when one Player kills another, and calls AddTeamScore. You will remove the call to AddTeamScore.

Within the OnUserDied() function, add a comment (–) to the start of the following lines:

fromUser:SendToScripts(“AddScore”, self.properties.killScore)

self:AddTeamScore(fromUser:FindScriptProperty(“team”), self.properties.teamKillScore)

So that the line appears as a comment. This is a technique called “commenting out” and is useful for developers to turn on or off a line of code without losing the code completely by deleting it. Your function should now read:

function GameScript:OnUserDied(userEntity, fromEntity, selfMsg, otherMsg, youKilledMsg)
	
	-- send game event to anyone who wanted
	self.properties.onUserDied:Send(userEntity, fromEntity, selfMsg, otherMsg, youKilledMsg)

	-- find user for player who killed us (if there is one)
	local fromUser = (fromEntity and fromEntity:IsA(Character)) and fromEntity:GetUser() or nil
	-- add score
	If fromUser then
-- fromUser:SendToScripts(“AddScore”, self.properties.killScore)
If self.properties.hasTeams and self.properties.teamKillScore > 0 then
	-- self:AddTeamScore(fromUser:FindScriptProperty(“team”), self.properties.teamKillScore)
end
end
	
end

You have removed the logic which adds scores to the team scoreboard and the individual player score, which means you can now write your own.

To add the scoring mechanic to the GoalScored function, you will borrow one of the lines of code you just commented out. 

Copy the following line of code (Ctrl+C) from the OnUserDied function:

self:AddTeamScore(fromUser:FindScriptProperty(“team”), self.properties.teamKillScore)

Return to your GoalScored function within gameScript and paste this before the Wait() function. Change the variable fromUser to theTrigger. 

self:AddTeamScore(theTrigger:FindScriptProperty(“team”), self.properties.teamKillScore)

Your GoalScored function should now read:

function GameScript:GoalScored(other, theTrigger)
	
	-- Check if the thing which entered the goal Trigger (other) was called football
	if other:GetName() == “football” then
		
		-- Use the scheduler
		self:Schedule(
			function()

				-- Apply the team score
				self:AddTeamScore(theTrigger:FindScriptProperty(“team”), self.properties.teamKillScore)

				-- wait for 3 seconds
				Wait(3.0)

		-- Set ball position to ballSpawn position
		other:SetPosition(self.properties.footballSpawn:GetPosition())

		-- Halt ball velocity and spin
		other:SetVelocity(Vector.Zero)
		other:SetAngularVelocity(Rotation.Zero)

	end -- end the code called within the scheduler
)

	end -- end ball name check

end -- end GoalScored function

The “theTrigger:FindScriptProperty(“team”)” part of the code is looking for a Property called “team” from any Script attached to the goalTeam1 Trigger. At the moment this Entity does not have any such Property

To add this, add a new Script to the goalTeam1 Trigger and name the Script goalTeamScript. The Script will be very simple and only include this Property. The whole code should read:

local GoalTeamScript = {}

GoalTeamScript.Properties = {
	{ name = “team”, type = “number”, default = 0 }
}

return GoalTeamScript

Set the Property to 1 in the Entity Editor.

Make sure you have saved the gameScript and the GoalTeamScript and test your game. After the game starts (there is a lobby countdown first) you should find that when you score your team gets a point. And the crowd goes wild!

Kicking The Ball

Dribbling in the game is possible with a bit of careful control, and this makes for quite a nice requirement of skill. Passing and shooting on the other hand are not currently possible. Let’s remedy this. 

To “kick” the ball you will need to check some conditions, and then apply some force to the ball in the direction the Player is facing. The way you will kick the ball is similar to how Russ builds a Blast Hammer in one  of the Crayta Livestreams: https://youtu.be/9286X65O-xo

First, open the Player in the Entity Editor using the dropdown (currently set to World).

Add a Trigger to the Player. Make its size 200 x 200 x 200 and move it just in front of the existing Trigger on the X axis (red arrow). Name the Trigger kickTrigger.

You will use this kickTrigger to determine if the football is close to the Player.

Create a Script Folder in the Player called KickLogic. Add a Script called KickLogicScript and open it in the Code Editor.

The main difference between Russ’ Blast Hammer Script is instead of launching a player it will apply an impulse force to the football. Start by adding some of the Properties that you will need:

KickLogicScript.Properties = {
	{ name = “strength”, type = “number”, default = 250 },
{ name = “kickTrigger”, type = “entity” },
{ name = “launchVecotr”, type = “vector” },
}

Saving the Script should make these Properties appear in the Entity Editor for the KickLogic Script Folder.

You will use the OnButtonPressed function, which will be automatically called when the User presses a button, to call a kick function which performs all the logic necessary to kick the ball.

Add the following code to the Script:

-- Called when the player presses a button
function KickLogicScript:OnButtonPressed(buttonName)
	-- Check this is the secondary button
	if buttonName == “secondary” then
		-- Call the Kick function
		self:Kick()
	end
end

-- Called from the OnButtonPressed function
function KickLogicScript:Kick()
	
-- Get a reference to the football
local ball = GetWorld():Find(“football”)

-- Check the ball exists
if ball then
	-- Check the ball is within the players kickTrigger
	If self.properties.kickTrigger:IsOverlapping(ball) then
		
		-- Store the balls position in local variable
		local ballPosition = ball:GetPosition()

		-- Work out the vector from the ball to the player
		local relativeVector = self:GetEntity():GetLookAtPos() - ballPosition

		-- “Normalize” this vector, so that it is simply directional and doesn’t
			-- refer to the distance (we don’t need the distance)
			local normalizedRelative = relativeVector:Normalize()

			-- Combine the direction any height trajectory we set in the launchVector
			local combinedVector = normalizedRelative + self.properties.launchVector

			-- Multiply by the strength of the kick
			local kickVector = combinedVector * self.properties.strength

			-- Apply this vector to the ball as an impulse
			ball:AddImpulse(kickVector)
		end
	end

end

Now set your Properties for this Script to the following:

  • strength: 60000
  • kickTrigger: kickTrigger (drag and drop the kickTrigger from the Player to set this)
  • launchVector: 0, 0, 1.2

Before we test this, we also need to remove the current functionality for the “secondary” button, which currently toggles iron sight mode on the gun you are holding.

Open the Gun Template using the Entity Explorer dropdown.

Use the Entity Editor to remove the ironSightItemScript from the Gun Template.

This will stop the “secondary” button from using the iron sight.

Now test your game and try kicking the ball. You can tweak the values in the Properties of the Script to suit how you want the game to “feel”.

Respawn Players When You Score

Scoring returns the ball to its start location, but what about the Players? To return the Players to their initial spawn point you can leverage the existing game mechanics. 

Add the following line of code within the gameScript:GoalScored function after you reset the physics on the football:

-- reset physics on the football
other:SetVelocity(Vector.Zero)
other: SetAngularVelocity(Rotation.Zero)

-- return players to original spawn points
GetWorld():ForEachUser(
	function(userEntity)
		userEntity:FindScript(“spawnUserScript”):SpawnInternal(false)
	end
end

Test your game and check that when the ball is respawned after scoring, so are the Players.

Building A Stadium

Now that you’ve coded the basic game mechanic it’s time to make this game a bit more interesting. Use the Voxel tools to build a stadium for your football game. Make sure to build a wall around the stadium to keep the ball in play and keep the tension mounting.

Think about theming the stadium. This game could be happening at any time, in any place. The fact that you are carrying a machine gun (which can still kill the opposing team) creates a somewhat…murderous…environment, so have fun with the setting.

To build another goal for the other team, extract your original goal across the pitch to make sure they are exactly opposite and exactly the same size. You can then remove a strip of blocks and extract it back to its original setting.

Copy the Trigger goalTeam1 Trigger. When you paste the duplicate you should find that it automatically changes the name to goalTeam2. Move it into the other goal, and remember to use the Entity Editor to change the Trigger’s GoalTeamScript to team 2.

…And that’s it! Remember to test your game, and try out both teams by inviting some friends along for a (deadly) kickabout.

Recap

Congratulations! In this tutorial you made a fun soccer game, made somewhat more sinister by the inclusion of automatic rifles.

You’ve developed skills in:

  • Respawning a ball and players to their original location
  • Modifying a game template to change the score mechanic of a team based game
  • Using triggers to score points

Adding Polish

Whilst this is the conclusion of the tutorial, there’s no reason to stop here. In fact this could be just the beginning. Flex your creative muscles by using what you’ve learned here, and in the other coding tutorials to:

  • Add a Script to the ball to store the last player to touch the ball and then Add Player Score to them when they score a goal
  • Add some spinning sawblades (like in Basic Tutorial 3: Events) or some laser meshes (such as in Basic Tutorial 5: Combat) to create even more danger in your game
  • Add player score when they hurt or kill another player to encourage more dangerous play
  • Add some exploding barrels (like in Coding Tutorial 1: Exploding Barrels) that explode when hit by the ball. You can use what you learned in the “Kicking The Ball” chapter to fire the ball across the field