2. A player you can move

In this part you make a character, tell the project which keys move it, and write the script that connects the two.

Make the player

The player is two entities: an empty one that does the moving, and a box under it that you can see.

  1. In the Hierarchy, press + and choose Create Empty. Name it Player.
  2. Set its Position to 0, 0, 6.
  3. In the inspector, type Player in the Tag box. Scripts will use the tag later to tell the player from everything else.
  4. Press Add Component and choose Character Controller. Set Radius to 0.4 and Center to 0, 0.9, 0. Leave Height at 1.8.

A Character Controller moves an entity the way a game character moves: it walks along the ground, climbs small steps, slides along walls and never falls over. Its shape is an upright capsule. With the centre at half the height, the entity's position is at the character's feet, which makes placing it easy.

Now the part you can see:

  1. Right-click Player and choose Create Cube. The cube is made as a child of the player. Name it Body.
  2. Set the body's Position to 0, 0.9, 0 and Scale to 0.8, 1.8, 0.8.
  3. Make a blue material called Suit and put it on the body.

A child moves with its parent. The body's position is measured from the player, so it stays on the player wherever the player goes. Do not add a collider to the body: the Character Controller is the player's shape.

The player selected, with its Character Controller in the inspector

Name the controls

Scripts do not ask about keys. They ask about actions, such as "Jump", and the project says which keys and buttons drive each action. That is what lets a player use a keyboard or a gamepad with the same script.

  1. Open File > Project Settings... and go to the Input tab.
  2. Type MoveX in the box at the bottom and press Add action.
  3. On the new row press +, then Press a key..., then press D.
  4. Press + again, tick The other way (-1), press Press a key... and press A.

MoveX is now an axis: 1 while D is held, -1 while A is held, and 0 for both or neither.

  1. Add the rest the same way:
ActionBindings
MoveXD, and A the other way
MoveZS, and W the other way
JumpSpace
RunLeftShift

MoveZ has S as the positive key because +Z points towards the camera in this engine, and forward is -Z.

To add a gamepad, press + and type Gamepad in the filter: Gamepad LeftX for MoveX, Gamepad LeftY for MoveZ, Gamepad A for Jump.

The Input tab of the project settings with the four actions

Write the script

  1. Select Player. Press Add Component and choose Scripts.
  2. Press New script. The editor writes Assets/Scripts/Player.lua and puts it on the player.
  3. In the Assets panel, open Assets/Scripts and double-click Player.lua. It opens in your code editor.

Replace what is there with a first version that only walks:

Lua
TypeScript
local Player = {}

Player.Fields = {
	-- Units per second.
	WalkSpeed = 4.5,
}

function Player:OnStart()
	self.Controller = self.Entity.CharacterController
end

function Player:OnUpdate()
	local move = Vec3(Input.GetValue("MoveX"), 0, Input.GetValue("MoveZ"))
	self.Controller:Move(move * self.WalkSpeed)
end

return Player

Save the file and press the green Play button above the viewport, or F5. Walk with W A S D. Press F5 again to stop.

The game playing in the viewport, which has a green edge

What the script says

A script is a Lua file that returns a table, or a TypeScript file that ends with export = DefineScript({ ... }) around the same table. The engine makes a copy of that table for each entity the script is on, and calls functions on it by name.

  • Player.Fields lists values the inspector shows. Select the player and you will find WalkSpeed under Scripts. Change it there and this entity uses the new number, without touching the file.
  • OnStart runs once when the game starts. self.Entity is the entity the script is on, and self.Entity.CharacterController is that entity's component. The Lua script keeps it in self.Controller to use every frame. The TypeScript one looks it up each frame and stops when it is missing: after that check, TypeScript knows it is there.
  • OnUpdate runs every frame. Input.GetValue gives each action's value from -1 to 1, the script builds a direction out of the two, and Move tells the controller how fast to walk that way.

Move takes a speed, not a distance. The controller keeps walking at that speed until told otherwise, and gravity is its own business.

Make it better

Three things are wrong with the first version. Walking diagonally is faster than walking straight, because the direction (1, 0, 1) is longer than (1, 0, 0). The player cannot run or jump. And it slides around without turning to face where it goes.

Change OnUpdate:

Lua
TypeScript
function Player:OnUpdate()
	local move = Vec3(Input.GetValue("MoveX"), 0, Input.GetValue("MoveZ"))
	-- Two keys at once must not be faster than one.
	if move:Length() > 1 then
		move = move:Normalized()
	end

	local speed = Input.IsDown("Run") and self.RunSpeed or self.WalkSpeed
	self.Controller:Move(move * speed)

	-- Face the way it walks.
	if move:Length() > 0.1 then
		self.Entity:Face(move)
	end

	if Input.WasPressed("Jump") then
		self.Controller:Jump(self.JumpSpeed)
	end
end

And add the two new fields:

Lua
TypeScript
Player.Fields = {
	-- Units per second.
	WalkSpeed = 4.5,
	RunSpeed = 8,
	JumpSpeed = Field.Number(6.5, { Min = 0, Max = 15 }),
}

You do not need to stop the game to try this. Save the file while the game is playing and the new code is in use at once.

  • Input.IsDown is true for as long as the action is held. Input.WasPressed is true only in the frame it went down, which is what a jump wants: one press, one jump.
  • a and b or c is Lua's way to pick one of two values on one line. TypeScript writes it a ? b : c.
  • Jump does nothing in the air, so there is no double jump to guard against.
  • Field.Number with Min and Max shows as a slider in the inspector.

The player still walks along the world's axes whatever the camera does. The next part fixes that, once there is a camera to ask.

What you learned

  • A parent and its children move together.
  • Scripts ask about named actions, and the project settings bind them to keys.
  • A script is a table of functions the engine calls: OnStart once, OnUpdate every frame.
  • Fields are values each entity can set in the inspector.
  • Scripts reload while the game runs.

Next: a camera that follows.