Asteroids prototype

Hi everyone !
Yesterday started a French game jam on the Asteroids theme.
I’ll put my questions in this thread.

I’m currently trying to script my ship movement but I struggle to make it even rotate.
Here is what I did so far:
I created a game object with an inplace sprite and a script file.
I created input bindings for left, right, up and down actions.
I added the game object in the main collection.
And here is the script I wrote so far:

function init(self)
	msg.post('.', 'acquire_input_focus')
	self.angle = 0
	self.angular_speed = 5
end

function on_input(self, action_id, action)
	if action_is == hash('left') then
		self.angle = self.angle - self.angular_speed
	end
	if action_is == hash('right') then
		self.angle = self.angle + self.angular_speed
	end
	go.set('.', 'euler.z', self.angle)
end

Edit: Ok. I found my error. I wrote action_is instead of action_id.I also inverted + and - in incrementing the angle.

1 Like

Got the ship rotating correctly.
Now, I’m trying to make it move forward (in the direction its facing) when I push the up action. I moved the controls from the on_input fonction to the update function in order to use the dt parameter. I’m sure there is plenty of things that I’m doing wrong and so far, the ship doesn’t move properly. It also continues to rotate even after I release the left or right actions but it stops when I press the up or down actions. Weird.
Here is my code:

function init(self)
	msg.post('.', 'acquire_input_focus')
	
	self.left = false
	self.right = false
	self.up = false
	self.down = false
	
	self.ANGULAR_SPEED = 200
	self.speed = 0
	self.THRUST_POWER = 200
	self.dx = 0
	self.dy = 0
end

function update(self, dt)
	local angle = go.get('.', 'euler.z')
	
	if self.left then
		angle = angle + self.ANGULAR_SPEED * dt
		go.set('.', 'euler.z', angle)
	end
	
	if self.right then
		angle = angle - self.ANGULAR_SPEED * dt
		go.set('.', 'euler.z', angle)
	end
	
	if self.up then
		self.speed = self.speed + self.THRUST_POWER * dt
	end
	
	if self.down then
		self.speed = self.speed - self.THRUST_POWER * dt
	end
	
	if self.speed < 0 then
		self.speed = 0
	end

	self.dx = math.cos(angle) * self.speed * dt
	self.dy = math.sin(angle) * self.speed * dt
	
	local position = go.get_position()
	position.x = position.x + self.dx
	position.y = position.y + self.dy
	go.set_position(position)
end

function on_input(self, action_id, action)
	self.left = action_id == hash('left')
	self.right = action_id == hash('right')
	self.up = action_id == hash('up')
	self.down = action_id == hash('down')
end

Edit: I forgot to convert from degrees to radians. But it still continues to rotate after I release the actions. Any explanation?

self.dx = math.cos(math.rad(angle)) * self.speed * dt
self.dy = math.sin(math.rad(angle)) * self.speed * dt

I added this code for the ship to wrap around the window:

	if position.x > 1280 then
		position.x = 0
	end
	if position.x < 0 then
		position.x = 1280
	end
	if position.y > 720 then
		position.y = 0
	end
	if position.y < 0 then
		position.y = 720
	end

Is there a way to get the width and height of the project windows instead of hardcoding the resolution?

Yes, from the render script. See my tutorial for an example.

1 Like

try:

self.left = action_id == hash('left') and not action.released`

about the size of the window:

window.set_listener(window_callback)
function window_callback(self, event, data)
	if event == window.WINDOW_EVENT_RESIZED then
		print("Window resized: ", data.width, data.height)
	end
end
1 Like

window.set_listener() only works when the window is resized. It won’t give you initial window size.

1 Like

Right.

For initial window settings you can also use:

sys.get_config("display.width")
sys.get_config("display.height")
1 Like

and not action.released worked. Thank you very much.
I’m pretty lost with those input actions and action_ids. Would you elaborate on those, if you can?

Did you read the manual here?

2 Likes

I doubled down the slow down speed for better controls:

if self.down then
	self.speed = self.speed - self.THRUST_POWER * 2 * dt
end

I made an asteroids game with Defold a long time ago. For the wrapping I made a separate script that I added to all the objects (though it would be a bit more efficient to use a module).

Wrapper Script Code
local wrap = require "main.framework.wrap_manager"
-- wrap_manager is a tiny module to store the half-sizes of the camera view. Gets set from the render script.
go.property("radius", 32)

function init(self)
	self.extents = {x = wrap.halfx + self.radius, y = wrap.halfy + self.radius}
	self.didWrap = false
end

function update(self, dt)
	self.pos = go.get_position()
	self.didWrap = false

	if self.pos.x > self.extents.x then
		self.pos.x = -self.extents.x
		self.didWrap = true
	elseif self.pos.x < -self.extents.x then
		self.pos.x = self.extents.x
		self.didWrap = true
	end

	if self.pos.y > self.extents.y then
		self.pos.y = -self.extents.y
		self.didWrap = true
	elseif self.pos.y < -self.extents.y then
		self.pos.y = self.extents.y
		self.didWrap = true
	end
	
	if self.didWrap then  go.set_position(self.pos)  end
end

If you wrap things exactly at the edge of the window then it will be very obvious when they disappear from one side and reappear at the other, and it could mean if your ship is near the edge of the screen that an asteroid will suddenly appear right on top of you (or vice versa). So I decided to wrap things some distance outside of the screen edges. Since there was a fairly wide size difference between my objects (large asteroids, player ship, bullets, etc.), I decided to make that distance a different minimum for each object. So that’s what the “radius” property is on my wrapper script. Yes, this can theoretically cause some discrepancies, since a small bullet can wrap before a large asteroid, but for me it wasn’t noticeable and this method “felt” the best.


I do input I do it similarly to what you have, only I use 1 for pressed and 0 for released (instead of true/false), and on update I combine these into an input vector.

function init(self)
	self.inputVec = vmath.vector3()
	self.up, self.down, self.left, self.right = 0, 0, 0, 0
end

function on_update(self, dt)
	self.inputVec.y = self.up - self.down
	self.inputVec.x = self.right - self.left
end

The most easily understandable way to do your on_input function is probably like this:

Click to show code
function on_input(self, action_id, action)
	if action.pressed then
		if action_id == hash("up") then
			self.up = 1
		elseif action_id == hash("down") then
			self.down = 1
		elseif action_id == hash("left") then
			self.left = 1
		elseif action_id == hash("right") then
			self.right = 1
		end
	elseif action.released then
		if action_id == hash("up") then
			self.up = 0
		elseif action_id == hash("down") then
			self.down = 0
		elseif action_id == hash("left") then
			self.left = 0
		elseif action_id == hash("right") then
			self.right = 0
		end
	end
end

If you’d rather your code be short, you can get a bit fancy and do something like this:

Click to show code
local inputMap = {
	[hash("up")] = "up",
	[hash("down")] = "down",
	[hash("left")] = "left",
	[hash("right")] = "right",
}

function on_input(self, action_id, action)
	if action.pressed or action.released then -- I prefer to filter out the unnecessary actions every frame.
		local mappedInputName = inputMap[action_id]
		if mappedInputName then
			self[mappedInputName] = action.value
		end
	end
end

The full version of your update and init should be something like this:

Click to show code
local FORWARD_VEC = vmath.vector3(0, 1, 0) -- Must match how your ship is designed in the editor.
local TURNSPEED = 4.6
local THRUST = 650
local DAMPING = 0.5

function init(self)
	self.inputVec = vmath.vector3()
	self.up, self.down, self.left, self.right = 0, 0, 0, 0
	self.vel = vmath.vector3() -- Linear velocity.
	self.forward = vmath.vector3(FORWARD_VEC) -- Forward vector.
end

function update(self, dt)
	self.inputVec.y = self.up - self.down
	self.inputVec.x = self.right - self.left
	self.pos = go.get_position()
	self.rot = go.get_rotation()

	if self.inputVec.x ~= 0 then
		local drot = vmath.quat_rotation_z(self.inputVec.x * -TURNSPEED * dt)
		self.rot = self.rot * drot
		go.set_rotation(self.rot)
	end

	self.forward = vmath.rotate(self.rot, FORWARD_VEC)

	if self.inputVec.y ~= 0 then -- Use `> 0` instead if you only want forward thrust.
		local accel = self.inputVec.y * THRUST * dt
		self.vel.x = self.vel.x + self.forward.x * accel
		self.vel.y = self.vel.y + self.forward.y * accel
	else -- Not thrusting, apply damping to velocity.
		self.vel.x = self.vel.x - self.vel.x * DAMPING * dt
		self.vel.y = self.vel.y - self.vel.y * DAMPING * dt
	end

	go.set_position(self.pos + self.vel * dt)
end

NOTE: All this code is completely untested, so it might not actually work if you copy-paste it in.


“Pro” Tip: If you accelerate your turning speed from zero to full turn speed over the first 0.15 sec that you hold a turn button, then you’ll get way better fine control over your aiming with no noticeable drawbacks, and your game will control better than 99% of the space games out there.

2 Likes

@COCO Yeah I read it some time ago but I’ll probably need a refresher.

Edit: I read it again but I’m still confused. I had to check the on_input callback documentation too but it doesn’t help either. Idon’t understand when that callback is called. I don’t understand how to combine action_id and action checking.

I’m sorry but I can’t manage to make sense of this input system.
I’ll try to explain my understanding about it so you’ll be able to correct my errors.

  • A physical input state is bound to an action in the input bindings.
  • For a specific game object to manage inputs, I have to send it the message "acquire_input_focus".
  • Then I can check an input state via its associated action in the on_input callback.
  • That callback has an action_id parameter that I can check to see if something happened to a specific input state.
  • I can use the action parameter to get more details about the input state.

Now, for a specific physical input state (let’s say a keyboard key), there are four cases I use in every engine I tried:

  • The key was not pressed before and is just being pressed. -> Just pressed state.
  • The key was pressed before and is still being pressed. -> Still pressed state.
  • The key was not pressed before and it is still not pressed. -> Still released state.
  • The key was pressed before and is just released. -> Just released state.

So tell me, how do I check all of these states in Defold. I can’t get how and when that on_input` callback is getting called.

For example, in Asteroids, I have to constantly check if a key is pressed to make the ship move or turn.
However, for firing a missile, I must fire only when I just press a key. Moreover, I’ll have to use some kind of cooldown to prevent firing missiles too fast. And in on_input callback, there is no such thing as a dt value.

I can’t manage to make sense of it in Defold.
If someone could help me explain in plain english all that stuff, I’d really appreciate it.

Edit: Here is a small drawing to show how I see that on_input callback.

The input system does not convey the state of a button, but a change in state of a button. So the action.pressed field indicates that this button has been pressed in this frame. Next frame action.pressed will become false even though the button is still pressed.
When the button is released you will get action.released == true, again indicating that a button has been released during this frame. Next frame it will become false even though the button is still released.

But if you hold a button long enough, Defold will send repeated event for a pressed button. Repeat interval is set in game.project.

Usually you create a set of boolean values indicating states of buttons that you can use inside an update() function. You can check my platformer sample for an example

2 Likes

OK, so to recap, it calls on_input callback only when the user just pressed or just released a key. But it also calls it repeatedly when the user keeps pressing a key. How can I make the difference?

action.repeated is a separate event, just ignore it.

1 Like

If you want to see my prototype, here is the link:
paper space
You’ll see that the inputs are jaggy. We can’t press a move/rotate control and fire at the same time.

@sergey.lerg wait! You mean that when I keep pressing a key, it will not be action.pressed? Just and only the first frame?

Yes, if I remember it right.

1 Like

Yes.


Try out this little script and see watch what you get in the output, maybe it will help you see how things work.

Input Tester Script
function init(self)
	msg.post(".", "acquire_input_focus")
	self.frame = 0
end

function update(self, dt)
	self.frame = self.frame + 1
end

function on_input(self, action_id, action)
	print(action_id, self.frame)
	print("   pressed: " .. tostring(action.pressed))
	print("   released: " .. tostring(action.released))
	print("   repeated: " .. tostring(action.repeated))
	print("   value: " .. tostring(action.value))
end
  1. You will get an input callback every frame for each key that you are holding down.
  2. On the first frame that you press a key, action.pressed will be true. All other times it will be false.
  3. On the frame that you release a key, action.released will be true. All other times it will be false.
  4. action.repeated will be true periodically, depending on your key repeat settings in game.project. It will generally NOT be true every frame.
  5. action.value will be 1 for every callback while the button is pressed, or 0 on the frame that it is released.

From your prototype, I’m guessing you have a problem with your conditionals (if, elseif, else) in your on_input function. You’re only using one button at a time instead of tracking them all separately.

3 Likes