189 | function GuiTopDownCamera:activate() |
190 | self.inputManager:setShowMouseCursor(true) |
191 | self:onInputModeChanged({self.inputManager:getLastInputMode()}) |
192 | |
193 | self:updatePosition() |
194 | |
195 | self.previousCamera = getCamera() |
196 | setCamera(self.camera) |
197 | |
198 | -- Leave the player so it is not visible and use player position as initial focus. |
199 | if self.controlledPlayer ~= nil then |
200 | local x, y, z = getTranslation(self.controlledPlayer.rootNode) |
201 | self.lastPlayerPos[1], self.lastPlayerPos[2], self.lastPlayerPos[3] = x, y, z |
202 | self.lastPlayerTerrainHeight = getTerrainHeightAtWorldPos(self.terrainRootNode, x, 0, z) |
203 | |
204 | -- self.controlledPlayer:onLeave() |
205 | self.controlledPlayer:setVisibility(true) |
206 | end |
207 | |
208 | self:resetToPlayer() |
209 | |
210 | self:registerActionEvents() |
211 | self.messageCenter:subscribe(MessageType.INPUT_MODE_CHANGED, self.onInputModeChanged, self) |
212 | |
213 | self.isActive = true |
214 | end |
352 | function GuiTopDownCamera:applyMovement(dt) |
353 | -- Smooth updates of camera |
354 | -- This lerps towards the target. Due to a constant alpha it will automatically |
355 | -- change faster if target is further away. |
356 | |
357 | local xChange = (self.targetCameraX - self.cameraX) / dt * 5 |
358 | if xChange < 0.0001 and xChange > -0.0001 then |
359 | self.cameraX = self.targetCameraX |
360 | else |
361 | self.cameraX = self.cameraX + xChange |
362 | end |
363 | |
364 | local zChange = (self.targetCameraZ - self.cameraZ) / dt * 5 |
365 | if zChange < 0.0001 and zChange > -0.0001 then |
366 | self.cameraZ = self.targetCameraZ |
367 | else |
368 | self.cameraZ = self.cameraZ + zChange |
369 | end |
370 | |
371 | local zoomChange = (self.targetZoomFactor - self.zoomFactor) / dt * 2 |
372 | if zoomChange < 0.0001 and zoomChange > -0.0001 then |
373 | self.zoomFactor = self.targetZoomFactor |
374 | else |
375 | self.zoomFactor = MathUtil.clamp(self.zoomFactor + zoomChange, 0, 1) |
376 | end |
377 | |
378 | |
379 | local tiltChange = (self.targetTiltFactor - self.tiltFactor) / dt * 5 |
380 | if tiltChange < 0.0001 and tiltChange > -0.0001 then |
381 | self.tiltFactor = self.targetTiltFactor |
382 | else |
383 | self.tiltFactor = MathUtil.clamp(self.tiltFactor + tiltChange, 0, 1) |
384 | end |
385 | |
386 | local rotateChange = (self.targetRotation - self.cameraRotY) / dt * 5 |
387 | if rotateChange < 0.0001 and rotateChange > -0.0001 then |
388 | self.cameraRotY = self.targetRotation |
389 | else |
390 | self.cameraRotY = self.cameraRotY + rotateChange |
391 | end |
392 | end |
117 | function GuiTopDownCamera:createCameraNodes() |
118 | local camera = createCamera("TopDownCamera", math.rad(60), 1, 4000) -- camera view point node |
119 | local cameraBaseNode = createTransformGroup("topDownCameraBaseNode")-- camera base node, look-at target |
120 | |
121 | link(cameraBaseNode, camera) |
122 | setRotation(camera, 0, math.rad(180) , 0) |
123 | setTranslation(camera, 0, 0, -5) |
124 | setRotation(cameraBaseNode, 0, 0 , 0) |
125 | setTranslation(cameraBaseNode, 0, 110, 0) |
126 | setFastShadowUpdate(camera, true) |
127 | |
128 | return camera, cameraBaseNode |
129 | end |
218 | function GuiTopDownCamera:deactivate() |
219 | self.isActive = false |
220 | |
221 | self.messageCenter:unsubscribeAll(self) |
222 | self:removeActionEvents() |
223 | |
224 | -- local showCursor = self.controlledPlayer == nil and self.controlledVehicle == nil |
225 | self.inputManager:setShowMouseCursor(false) |
226 | |
227 | -- restore player position and previous camera |
228 | if self.controlledPlayer ~= nil then |
229 | local x,y,z = unpack(self.lastPlayerPos) |
230 | local currentPlayerTerrainHeight = getTerrainHeightAtWorldPos(self.terrainRootNode, x,0,z) |
231 | local deltaTerrainHeight = currentPlayerTerrainHeight - self.lastPlayerTerrainHeight |
232 | if deltaTerrainHeight > 0 then |
233 | y = y + deltaTerrainHeight |
234 | end |
235 | |
236 | self.controlledPlayer:moveRootNodeToAbsolute(x, y, z) |
237 | -- self.controlledPlayer:onEnter(true) |
238 | self.controlledPlayer:setVisibility(false) |
239 | end |
240 | |
241 | if self.previousCamera ~= nil then |
242 | setCamera(self.previousCamera) |
243 | self.previousCamera = nil |
244 | end |
245 | end |
404 | function GuiTopDownCamera:getMouseEdgeScrollingMovement() |
405 | local moveMarginStartX = 0.015 + 0.005 |
406 | local moveMarginEndX = 0.015 |
407 | local moveMarginStartY = 0.015 + 0.005 |
408 | local moveMarginEndY = 0.015 |
409 | |
410 | local moveX, moveZ = 0, 0 |
411 | |
412 | if self.mousePosX >= 1 - moveMarginStartX then |
413 | moveX = math.min((moveMarginStartX - (1 - self.mousePosX)) / (moveMarginStartX - moveMarginEndX), 1) |
414 | elseif self.mousePosX <= moveMarginStartX then |
415 | moveX = -math.min((moveMarginStartX - self.mousePosX) / (moveMarginStartX - moveMarginEndX), 1) |
416 | end |
417 | |
418 | if self.mousePosY >= 1 - moveMarginStartY then |
419 | moveZ = math.min((moveMarginStartY - (1 - self.mousePosY)) / (moveMarginStartY - moveMarginEndY), 1) |
420 | elseif self.mousePosY <= moveMarginStartY then |
421 | moveZ = -math.min((moveMarginStartY - self.mousePosY) / (moveMarginStartY - moveMarginEndY), 1) |
422 | end |
423 | |
424 | return moveX, moveZ |
425 | end |
486 | function GuiTopDownCamera:mouseEvent(posX, posY, isDown, isUp, button) |
487 | if self.lastActionFrame >= g_time or self.cursorLocked then |
488 | return |
489 | end |
490 | |
491 | -- Mouse move only happens when other actions did not |
492 | if self.isCatchingCursor then |
493 | self.isCatchingCursor = false |
494 | self.inputManager:setShowMouseCursor(true) |
495 | |
496 | -- force warp to get rid of invisible position |
497 | wrapMousePosition(0.5, 0.5) |
498 | |
499 | self.mousePosX = 0.5 |
500 | self.mousePosY = 0.5 |
501 | else |
502 | if self.isMouseMode then |
503 | self.mousePosX = posX |
504 | self.mousePosY = posY |
505 | end |
506 | end |
507 | end |
581 | function GuiTopDownCamera:onRotate(_, inputValue, _, isAnalog, isMouse) |
582 | if isMouse and self.mouseDisabled then |
583 | return |
584 | end |
585 | |
586 | -- Do not show cursor and clip to center of screen so that cursor does not |
587 | -- overlap with edge scrolling |
588 | if isMouse and inputValue ~= 0 then |
589 | self.lastActionFrame = g_time |
590 | |
591 | if not self.isCatchingCursor then |
592 | self.inputManager:setShowMouseCursor(false) |
593 | self.isCatchingCursor = true |
594 | end |
595 | end |
596 | |
597 | -- Analog has very small steps |
598 | if isMouse and isAnalog then |
599 | inputValue = inputValue * 3 |
600 | end |
601 | |
602 | self.inputRotate = -inputValue * 3 / g_currentDt * 16 |
603 | end |
607 | function GuiTopDownCamera:onTilt(_, inputValue, _, isAnalog, isMouse) |
608 | if isMouse and self.mouseDisabled then |
609 | return |
610 | end |
611 | |
612 | -- Do not show cursor and clip to center of screen so that cursor does not |
613 | -- overlap with edge scrolling |
614 | if isMouse and inputValue ~= 0 then |
615 | self.lastActionFrame = g_time |
616 | |
617 | if not self.isCatchingCursor then |
618 | self.inputManager:setShowMouseCursor(false) |
619 | self.isCatchingCursor = true |
620 | end |
621 | end |
622 | |
623 | -- Analog has very small steps |
624 | if isMouse and isAnalog then |
625 | inputValue = inputValue * 3 |
626 | end |
627 | |
628 | self.inputTilt = inputValue * 3 |
629 | end |
521 | function GuiTopDownCamera:registerActionEvents() |
522 | local _, eventId = self.inputManager:registerActionEvent(InputAction.AXIS_MOVE_SIDE_PLAYER, self, self.onMoveSide, false, false, true, true) |
523 | self.inputManager:setActionEventTextPriority(eventId, GS_PRIO_VERY_LOW) |
524 | self.inputManager:setActionEventTextVisibility(eventId, false) |
525 | self.eventMoveSide = eventId |
526 | |
527 | _, eventId = self.inputManager:registerActionEvent(InputAction.AXIS_MOVE_FORWARD_PLAYER, self, self.onMoveForward, false, false, true, true) |
528 | self.inputManager:setActionEventTextPriority(eventId, GS_PRIO_VERY_LOW) |
529 | self.inputManager:setActionEventTextVisibility(eventId, false) |
530 | self.eventMoveForward = eventId |
531 | |
532 | _, eventId = self.inputManager:registerActionEvent(InputAction.AXIS_CONSTRUCTION_CAMERA_ZOOM, self, self.onZoom, false, false, true, true) |
533 | self.inputManager:setActionEventTextPriority(eventId, GS_PRIO_LOW) |
534 | _, eventId = self.inputManager:registerActionEvent(InputAction.AXIS_CONSTRUCTION_CAMERA_ROTATE, self, self.onRotate, false, false, true, true) |
535 | self.inputManager:setActionEventTextPriority(eventId, GS_PRIO_LOW) |
536 | _, eventId = self.inputManager:registerActionEvent(InputAction.AXIS_CONSTRUCTION_CAMERA_TILT, self, self.onTilt, false, false, true, true) |
537 | self.inputManager:setActionEventTextPriority(eventId, GS_PRIO_LOW) |
538 | end |
446 | function GuiTopDownCamera:updateMovement(dt) |
447 | self.targetZoomFactor = MathUtil.clamp(self.targetZoomFactor - self.inputZoom * 0.2, 0, 1) |
448 | self.targetRotation = self.targetRotation + dt * self.inputRotate * GuiTopDownCamera.ROTATION_SPEED |
449 | self.targetTiltFactor = MathUtil.clamp(self.targetTiltFactor + self.inputTilt * dt * GuiTopDownCamera.ROTATION_SPEED, 0, 1) |
450 | |
451 | local moveX = self.inputMoveSide * dt |
452 | local moveZ = -self.inputMoveForward * dt -- inverted to make it consistent |
453 | |
454 | -- When touching the edge with mouse cursor, move |
455 | if moveX == 0 and moveZ == 0 and self.isMouseEdgeScrollingActive then |
456 | moveX, moveZ = self:getMouseEdgeScrollingMovement() |
457 | end |
458 | |
459 | -- make movement faster when zoomed out |
460 | local zoomMovementSpeedFactor = GuiTopDownCamera.MOVE_SPEED_FACTOR_NEAR + self.zoomFactor * (GuiTopDownCamera.MOVE_SPEED_FACTOR_FAR - GuiTopDownCamera.MOVE_SPEED_FACTOR_NEAR) |
461 | moveX = moveX * zoomMovementSpeedFactor |
462 | moveZ = moveZ * zoomMovementSpeedFactor |
463 | |
464 | -- note: we use the actual current camera rotation to define the direction, instead of the target location |
465 | local dirX = math.sin(self.cameraRotY) * moveZ + math.cos(self.cameraRotY) * -moveX |
466 | local dirZ = math.cos(self.cameraRotY) * moveZ - math.sin(self.cameraRotY) * -moveX |
467 | |
468 | local limit = self.terrainSize * 0.5 - GuiTopDownCamera.TERRAIN_BORDER |
469 | local moveFactor = dt * GuiTopDownCamera.MOVE_SPEED |
470 | self.targetCameraX = MathUtil.clamp(self.targetCameraX + dirX * moveFactor, -limit, limit) |
471 | self.targetCameraZ = MathUtil.clamp(self.targetCameraZ + dirZ * moveFactor, -limit, limit) |
472 | |
473 | self:applyMovement(dt) |
474 | self:updatePosition() |
475 | end |
300 | function GuiTopDownCamera:updatePosition() |
301 | local samplingGridStep = 2 -- terrain sampling step distance in meters |
302 | local cameraTargetHeight = self.waterLevelHeight |
303 | |
304 | -- sample the terrain height around the camera |
305 | for x = -samplingGridStep, samplingGridStep, samplingGridStep do |
306 | for z = -samplingGridStep, samplingGridStep, samplingGridStep do |
307 | local sampleTerrainHeight = getTerrainHeightAtWorldPos(self.terrainRootNode, self.cameraX + x, 0, self.cameraZ + z) |
308 | cameraTargetHeight = math.max(cameraTargetHeight, sampleTerrainHeight) |
309 | end |
310 | end |
311 | |
312 | cameraTargetHeight = cameraTargetHeight + GuiTopDownCamera.CAMERA_TERRAIN_OFFSET |
313 | |
314 | -- Tilt factor decides position between min and max. Min and max depend on zoom level |
315 | local rotMin = math.rad(GuiTopDownCamera.ROTATION_MIN_X_NEAR + (GuiTopDownCamera.ROTATION_MIN_X_FAR - GuiTopDownCamera.ROTATION_MIN_X_NEAR) * self.zoomFactor) |
316 | local rotMax = math.rad(GuiTopDownCamera.ROTATION_MAX_X_NEAR + (GuiTopDownCamera.ROTATION_MAX_X_FAR - GuiTopDownCamera.ROTATION_MAX_X_NEAR) * self.zoomFactor) |
317 | local rotationX = rotMin + (rotMax - rotMin) * self.tiltFactor |
318 | |
319 | -- Distance to target depends fully on zoom level |
320 | local cameraZ = GuiTopDownCamera.DISTANCE_MIN_Z + self.zoomFactor * GuiTopDownCamera.DISTANCE_RANGE_Z |
321 | |
322 | setTranslation(self.camera, 0, 0, cameraZ) |
323 | setRotation(self.cameraBaseNode, rotationX, self.cameraRotY, 0) |
324 | setTranslation(self.cameraBaseNode, self.cameraX, cameraTargetHeight, self.cameraZ) |
325 | |
326 | -- check if new camera position is close to or even under terrain and lift it if needed |
327 | local cameraX, cameraY |
328 | cameraX, cameraY, cameraZ = getWorldTranslation(self.camera) |
329 | local terrainHeight = self.waterLevelHeight |
330 | for x = -samplingGridStep, samplingGridStep, samplingGridStep do |
331 | for z = -samplingGridStep, samplingGridStep, samplingGridStep do |
332 | local y = getTerrainHeightAtWorldPos(self.terrainRootNode, cameraX + x, 0, cameraZ + z) |
333 | |
334 | local hit, _, hitY, _ = RaycastUtil.raycastClosest(cameraX + x, y + 100, cameraZ + z, 0, -1, 0, 100, CollisionMask.ALL - CollisionMask.TRIGGERS) |
335 | if hit then |
336 | y = hitY |
337 | end |
338 | |
339 | terrainHeight = math.max(terrainHeight, y) |
340 | end |
341 | end |
342 | |
343 | -- TODO instead we should tilt the camera up to clear the terrain... |
344 | if cameraY < terrainHeight + GuiTopDownCamera.GROUND_DISTANCE_MIN_Y then |
345 | cameraTargetHeight = cameraTargetHeight + (terrainHeight - cameraY + GuiTopDownCamera.GROUND_DISTANCE_MIN_Y) |
346 | setTranslation(self.cameraBaseNode, self.cameraX, cameraTargetHeight, self.cameraZ) |
347 | end |
348 | end |