Scrolling and dragging the camera (SOLVED)

I’m trying to create a world map that the player can scroll around by clicking and dragging. The world map is made up of several different game objects, so I want to use a global variable to move them all at the same time.

(I realise this would be easier if I had just made a movable camera, but I have spent a lot of time getting this functional and I just need that last thing, and i have a feeling that a camera’s movement would be calculated in the same way.)

EDIT: THIS PART IS NOT TRUE. SCREEN DX AND SCREEN DY ARE NOT RELATED
I believe I need to use the screen_dx and screen_dy values, which give the difference between the mouse position in the last update and the mouse position in the current update. However, both return nil. Is there a variable which returns what I’m looking for?

I have just realised that I didn’t really understand screen_dx. I’m looking into this.

Yes, it would be a lot easier with a camera component. You would not have to move all game objects very frame, you simply move one game object with the camera component.

Okay… as it turns out, I have the same problem with the moveable camera. It’s like this: I need to set the location of the camera according to its start point and the yoffset which comes from the player input.

I’ve made a global variable called yoffset which measures the distance between the place where the player clicked, and the place where the player moved the mouse to.

mouse is not clicked = yoffset is 0
mouse is clicked = yoffset isn0
mouse is clicked and dragged 10 pixels left = yoffset is -10
mouse is released = yoffset returns to 0

This works, but even when the player stops moving the mouse, the offset is applied over and over again and the camera keeps moving.

function update(self, dt)
local p= go.get_position()
if p.y ~= originp.y + offsetter_y and p.y < 400 and offsetter_y >0 then
go.set_position(vmath.vector3(p.x, p.y+offsetter_y, p.z))
end
if p.y ~= originp.y + offsetter_y and p.y > -1200 and offsetter_y <0 then
go.set_position(vmath.vector3(p.x, p.y+offsetter_y, p.z))
end
end

I would appreciate any advice.

once again, I am scuppered by human error.

The correct code is in fact

function update(self, dt)
local p= go.get_position()
if p.y ~= originp.y + offsetter_y and p.y < 400 and offsetter_y >0 then
go.set_position(vmath.vector3(p.x, originp.y+offsetter_y, p.z))
end
if p.y ~= originp.y + offsetter_y and p.y > -1200 and offsetter_y <0 then
go.set_position(vmath.vector3(p.x, originp.y+offsetter_y, p.z))
end
end

Ok, so you got it working? Can I mark this as solved?

Not really. This is still pretty buggy and doesn’t work as well as it could. If anyone has any examples to share I would love to have a look at them (what I am looking for is being able to scroll around the screen by clicking and dragging using the mouse).

I dont have an example, but i would notice, that you probably should set yoffset to 0 every time when you move tha camera. So basically when you move the mouse for another 10 pixels, it will move the world for 10 pixels, not for 20 (10 on the previous frame which you already moved and 10 on the new one).
Also there might be a problem of scaling, where the camera moves faster/slower than the mouse, you’ll have to convert screen coordinates to world then somehow.

Not tested example

-- Update function
if offsety ~= 0 then
  local new_pos = go.get_position() + vmath.vector3(0, offsety, 0)
  go.set_position(new_pos)
  offsety = 0
end

And in the on_input function i track the mouse movements and if it is clicked or not
There i do offsety = offsety + new_offset, because there might be some weird stuff that calls on_input twice before the update. Probably shouldn’t be, but just in case.

local offsety = 0
local previous_y = nil

function on_input(self, action_id, action)
  if action_id == nil then
    -- TODO add checking if the mouse if pressed there, you already have it anyway
    local current_mouse_y = action.screen_y -- might want to try and use action.y
    local delta = current_mouse_y - (previous_y or current_mouse_y)
    offsety = offsety + delta 
    previous_y = current_mouse_y
  end

UPD: i suppose you can just move the camera in the on_input immediately, without storing offset externally (still need previous mouse position though

I’ve created an example of how to click and drag to scroll. The example also shows how to detect and handle a fling gesture where the camera continues to move and gradually slows down:

Source: https://github.com/britzl/publicexamples/tree/master/examples/drag_to_scroll
Try it: http://britzl.github.io/publicexamples/drag_to_scroll/index.html

6 Likes

This is so very useful. Thank you. I can mark it as solved myself, right?

Edit: I have changed the title of the thread to avoid confusion (the earlier version referenced the screen_dx and screen_dy values but that was a red herring)

(my next challenge is to integrate limits in all four directions)

I’ve made a little modification to your code that creates a maximum scrollable area.

if the camera is moved outside of the maximum scrollable area, flinging doesn’t work, and the camera bounces back to just inside the maximum scrollable area. Works in all four directions. Maximum scrollable area is defined by 4 properties at the start of the script.

go.property("fling_enabled", true)
go.property("fling_distance_threshold", 2000)	-- pixels per second
go.property("xmin", -120) -- these four attributes control the size of the scrollable part of the display
go.property("xmax", 230)
go.property("ymin", -300)
go.property("ymax", 250)
go.property("within", true)
go.property("sliding", false)

local function slidedone(self, url, property)
	go.set("#script", "within", true)
	go.set("#script", "sliding", false)
end

function init(self)
	msg.post("camera", "acquire_camera_focus")
	msg.post(".", "acquire_input_focus")
	self.fling = vmath.vector3()
	
end

function final(self)
	msg.post("camera", "release_camera_focus")
	msg.post(".", "release_input_focus")
end

function update(self, dt)
	if not self.drag and self.fling_enabled and self.within then
		go.set_position(go.get_position() + self.fling)
		self.fling = vmath.lerp(0.1, self.fling, vmath.vector3()) 
	end
	if not self.drag and not self.within and not self.sliding then 
		if go.get_position().y >= self.ymax then 
			go.animate(".", "position.y", go.PLAYBACK_ONCE_FORWARD, self.ymax-1, go.EASING_OUTEXPO, 0.2, 0, slidedone)
			go.set("#script", "sliding", true)
			go.set("#script", "fling_enabled", false)
		end
		if go.get_position().y <= self.ymin then
			go.animate(".", "position.y", go.PLAYBACK_ONCE_FORWARD, self.ymin+1, go.EASING_OUTEXPO, 0.2, 0, slidedone)
			go.set("#script", "sliding", true)
			go.set("#script", "fling_enabled", false)
		end
		if go.get_position().x >= self.xmax then
			go.animate(".", "position.x", go.PLAYBACK_ONCE_FORWARD, self.xmax-1, go.EASING_OUTEXPO, 0.2, 0, slidedone)
			go.set("#script", "sliding", true)
			go.set("#script", "fling_enabled", false)
		end
		if go.get_position().x <= self.xmin then 
			go.animate(".", "position.x", go.PLAYBACK_ONCE_FORWARD, self.xmin+1, go.EASING_OUTEXPO, 0.2, 0, slidedone)
			go.set("#script", "sliding", true)
			go.set("#script", "fling_enabled", false)
		end
		
	end
end


function on_input(self, action_id, action)
	if action.pressed then
		self.drag = true
		self.pressed_pos = vmath.vector3(action.x, action.y, 0)
		self.pressed_time = socket.gettime()
		self.camera_pos = go.get_position()
	elseif action.released then
		self.drag = false
		if self.fling_enabled and self.within then
			local released_time = socket.gettime()
			local released_pos = vmath.vector3(action.x, action.y, 0)
			local delta_time = released_time - self.pressed_time
			local distance = vmath.length(released_pos - self.pressed_pos)
			local velocity = distance / delta_time
			if velocity >= self.fling_distance_threshold then
				local direction = vmath.normalize(self.pressed_pos - released_pos)
				self.fling = direction * velocity * 0.02
			end
		end
		if go.get_position().x >= self.xmin and go.get_position().x <= self.xmax and go.get_position().y >= self.ymin and go.get_position().y <= self.ymax then
			go.set("#script", "within", true)
			else
			go.set("#script", "within", false)
		end
	end

	
	if self.drag then
		local mouse_pos = vmath.vector3(action.x, action.y, 0)
		go.set_position(self.camera_pos + self.pressed_pos - vmath.vector3(action.x, action.y, 0))		
		if self.fling_enabled then
			go.set("#script", "sliding", false)
			end
	end
end
3 Likes

Cool! Don’t forget local in front of the slideone function declaration:

local function slidedone(self, url, property)

If you don’t put local in front of the function it will become global and that is rarely a good idea.

For some reason, with the above code, if i make the function local, it doesn’t work. I don’t know enough about local function to know why.

edit: Okay, I put the local function at the top of the script and now it works. I’ll edit the sample I put up.

You need to declare the function before you use it. This means that slideone() must come before the update() function where it is used.

You should study a tutorial on Lua scopes to better understand how they work. It’s a bad idea to ignore locals and declare everything as global.

1 Like