Third Person Camera

Sorry for bringing up an old topic, but I recently came across this script and it’s a great resource from what I can tell so far by using it in my game.

However I have one issue with it, and it is that when the camera knows there’s an object behind it using the Rays, it more or less pulses back and forth instead of getting closer to the character smoothly (kinda hard to explain properly). I’ve tried adjusting the smooth values but it still seems to occur.

Another thing, would there be a way to take the scroll to zoom part of the script? I’m not too great with Python so I wouldn’t know how to make it into a separate script.

Last thing, in my game, the script will work when the middle mouse button is held down using States. When I hold middle mouse again, the camera will snap to the mouse location. How would I make it so that the camera will not move upon holding the middle mouse button and only move if the mouse is moving?

when the camera knows there’s an object behind it using the Rays, it more or less pulses back and forth instead of getting closer to the character smoothly

Hard to say what the exact issue is and how to fix it without an example. Is the object behind the camera moving or does it have a complex collision mesh? The camera collision rays will generally work best when you only use them with static objects using simple collision shapes.

Another thing, would there be a way to take the scroll to zoom part of the script? I’m not too great with Python so I wouldn’t know how to make it into a separate script.

This easiest way is just to say the ZOOM_STEP constant near the top of the script to 0.

How would I make it so that the camera will not move upon holding the middle mouse button and only move if the mouse is moving?

I assume you mean you want to prevent the mouse from controlling the camera while the middle mouse button is being held down (but still allow the camera to move closer or further from the target if it detects collisions. If so, you can try replacing the script with this one to see if it does what you want (you shouldn’t need to use states).

# Third Person Camera - Made by Mobious

import bge
import math
import mathutils as mathu

# constants
# Feel free to tweak these.
SENSITIVITY = .003      # mouse sensitivity
S = .8                  # mouse smoothing
INV_X = 1               # invert x-axis
INV_Y = 1               # invert y-axis
MAX_DIST = 25.0         # maximum distance from player
MIN_DIST = 5            # minimum distance to obstacle or player
DRCAP = .5              # distance restore speed cap
RSMOOTH = 20            # camera range smoothing
FSMOOTH = RSMOOTH / 3   # front obstruction smoothing
ZSMOOTH = 13            # zoom smoothing
LRSMOOTH = 10           # camera LR smoothing
ZOOM_STEP = 1.3         # distance to zoom on mousewheel
Z_OFFSET = 1.0          # Z offset from player
HOR_OFFSET = 0.0        # Horizontal offset factor
MIN_ANGLE = math.pi/20  # minimum angle camera can be from verticle
PROP = 'block_camera'   # property that block camera

# Change 'Player' to whatever object you want the camera to track
scene = bge.logic.getCurrentScene()
player = scene.objects['Player']

# controls
ZoomIn = bge.events.WHEELUPMOUSE
ZoomOut = bge.events.WHEELDOWNMOUSE
ToggleLR = bge.events.MIDDLEMOUSE

def main():
    
    cont = bge.logic.getCurrentController()
    own = cont.owner
    
    mouse = cont.sensors['Mouse']
    rear = cont.sensors['rear']
    front = cont.sensors['front']
        
    # initialize vars
    if 'x' not in own:
        own['x'] = 0.0
        own['y'] = math.pi / 3
        own['dist'] = MAX_DIST
        x = bge.render.getWindowWidth() // 2
        y = bge.render.getWindowHeight() // 2
        own['size'] = (x,y)
        bge.render.setMousePosition(x,y)
        own['zoom'] = MAX_DIST
        own['oldx'] = 0.0
        own['oldy'] = 0.0
        rear.range = MAX_DIST
        front.range = MAX_DIST - MIN_DIST
        rear.propName = PROP
        front.propName = PROP
        own['LR'] = HOR_OFFSET
        own['LRinv'] = 1
        return
    
    # check and correct for camera obstructions using smoothing
    # obstacle in front of camera
    if front.positive:
        dist = own.getDistanceTo(front.hitPosition)
        if (own['dist'] - dist) > MIN_DIST:
            own['dist'] -= (dist + MIN_DIST) / FSMOOTH
        
    # obstacle behind camera
    elif rear.positive:
        dist = own.getDistanceTo(rear.hitPosition)
        if dist > MIN_DIST:
            if own['dist'] - own['zoom'] < 0:
                own['dist'] += (dist - min(MIN_DIST, own['zoom'])) / RSMOOTH
        elif own.getDistanceTo(player) > MIN_DIST:
            own['dist'] -= min(MIN_DIST - dist, own['dist'] - MIN_DIST) / RSMOOTH
    elif own['dist'] < own['zoom']: # restore max distance
        dist = (own['zoom'] - own['dist'])
        own['dist'] += dist / ZSMOOTH
    if own['dist'] > own['zoom']: # zoom in
        own['dist'] -= (own['dist'] - own['zoom']) / ZSMOOTH
    front.range = own['dist'] - MIN_DIST

    # mouse movement
    keys = bge.logic.mouse.events
    keys.update(bge.logic.keyboard.events)
    xpos = own['size'][0]
    ypos = own['size'][1]
    if keys[bge.events.MIDDLEMOUSE] == bge.logic.KX_INPUT_NONE:
        x = INV_X*(xpos - mouse.position[0])
        y = -INV_Y*(ypos - mouse.position[1])
        x *= SENSITIVITY
        y *= SENSITIVITY
        bge.render.setMousePosition(xpos,ypos)
    elif keys[bge.events.MIDDLEMOUSE] == bge.logic.KX_INPUT_JUST_RELEASED:
        bge.render.setMousePosition(xpos,ypos)
        x = y = 0
    else:
        x = y = 0
    
    # smooth movement
    own['oldx'] = (own['oldx']*S + x*(1-S))
    own['oldy'] = (own['oldy']*S + y*(1-S))
    x = own['oldx']
    y = own['oldy']
    
    # zoom controls
    if keys[ZoomIn] and own['zoom'] >= MIN_DIST + ZOOM_STEP:
        own['zoom'] -= ZOOM_STEP
    elif keys[ZoomOut] and own['zoom'] <= MAX_DIST - ZOOM_STEP:
        own['zoom'] += ZOOM_STEP
        
    # update angles
    inv = False
    own['x'] += x
    own['y'] -= y
    
    if own['x'] > math.pi:
        own['x'] -= 2*math.pi
        inv = True
    elif own['x'] <= -2*math.pi:
        own['x'] += 2*math.pi
        inv = True
    if own['y'] > math.pi - 2*MIN_ANGLE:
        own['y'] = math.pi - 2*MIN_ANGLE
    elif own['y'] < 0.0 + MIN_ANGLE:
        own['y'] = 0.0 + MIN_ANGLE
    
    # LR toggling and smoothing
    if keys[ToggleLR] == 1:
        own['LRinv'] *= -1
    dist = own['LRinv']*HOR_OFFSET - own['LR']
    own['LR'] += dist / LRSMOOTH
    dist = own['LR'] / math.pow(own['dist'], 2)
    
    # calculate and set new camera position
    own['tdist'] = own['dist']*math.fabs(math.cos(math.pi/2 - own['y']))
    zshift = own['dist']*math.sin(math.pi/2 - own['y'])
    z = player.worldPosition[2] + zshift + Z_OFFSET
    yshift = own['tdist']*-math.cos(own['x'])
    y = player.worldPosition[1] + yshift + dist*math.sin(own['x'])
    xshift = own['tdist']*math.sin(own['x'])
    x = player.worldPosition[0] + xshift + dist*math.cos(own['x'])
    own.worldPosition = [x,y,z]
    
    #set new camera angle
    vec = mathu.Vector((own['y'], 0, own['x']))
    own.localOrientation = vec

Thank you, the script works for the most part and you’re right, I don’t need to use States which is an added bonus for me. However when the middle mouse button isn’t held down, the mouse is stuck in the centre of the screen. How would you go about making it so the cursor can be moved?

About the camera collision, I recorded the problem.

Use this script if you want the mouse to always be able to move around.

# Third Person Camera - Made by Mobious

import bge
import math
import mathutils as mathu

# constants
# Feel free to tweak these.
SENSITIVITY = .003      # mouse sensitivity
S = .8                  # mouse smoothing
INV_X = 1               # invert x-axis
INV_Y = 1               # invert y-axis
MAX_DIST = 25.0         # maximum distance from player
MIN_DIST = 5            # minimum distance to obstacle or player
DRCAP = .5              # distance restore speed cap
RSMOOTH = 20            # camera range smoothing
FSMOOTH = RSMOOTH / 3   # front obstruction smoothing
ZSMOOTH = 13            # zoom smoothing
LRSMOOTH = 10           # camera LR smoothing
ZOOM_STEP = 1.3         # distance to zoom on mousewheel
Z_OFFSET = 1.0          # Z offset from player
HOR_OFFSET = 0.0        # Horizontal offset factor
MIN_ANGLE = math.pi/20  # minimum angle camera can be from verticle
PROP = 'block_camera'   # property that block camera

# Change 'Player' to whatever object you want the camera to track
scene = bge.logic.getCurrentScene()
player = scene.objects['Player']

# controls
ZoomIn = bge.events.WHEELUPMOUSE
ZoomOut = bge.events.WHEELDOWNMOUSE
ToggleLR = bge.events.MIDDLEMOUSE

def main():
    
    cont = bge.logic.getCurrentController()
    own = cont.owner
    
    mouse = cont.sensors['Mouse']
    rear = cont.sensors['rear']
    front = cont.sensors['front']
        
    # initialize vars
    if 'x' not in own:
        own['x'] = 0.0
        own['y'] = math.pi / 3
        own['dist'] = MAX_DIST
        x = bge.render.getWindowWidth() // 2
        y = bge.render.getWindowHeight() // 2
        own['size'] = (x,y)
        bge.render.setMousePosition(x,y)
        own['zoom'] = MAX_DIST
        own['oldx'] = 0.0
        own['oldy'] = 0.0
        own['prevPos'] = [0, 0]
        rear.range = MAX_DIST
        front.range = MAX_DIST - MIN_DIST
        rear.propName = PROP
        front.propName = PROP
        own['LR'] = HOR_OFFSET
        own['LRinv'] = 1
        return
    
    # check and correct for camera obstructions using smoothing
    # obstacle in front of camera
    if front.positive:
        dist = own.getDistanceTo(front.hitPosition)
        if (own['dist'] - dist) > MIN_DIST:
            own['dist'] -= (dist + MIN_DIST) / FSMOOTH
        
    # obstacle behind camera
    elif rear.positive:
        dist = own.getDistanceTo(rear.hitPosition)
        if dist > MIN_DIST:
            if own['dist'] - own['zoom'] < 0:
                own['dist'] += (dist - min(MIN_DIST, own['zoom'])) / RSMOOTH
        elif own.getDistanceTo(player) > MIN_DIST:
            own['dist'] -= min(MIN_DIST - dist, own['dist'] - MIN_DIST) / RSMOOTH
    elif own['dist'] < own['zoom']: # restore max distance
        dist = (own['zoom'] - own['dist'])
        own['dist'] += dist / ZSMOOTH
    if own['dist'] > own['zoom']: # zoom in
        own['dist'] -= (own['dist'] - own['zoom']) / ZSMOOTH
    front.range = own['dist'] - MIN_DIST

    # mouse movement
    keys = bge.logic.mouse.events
    keys.update(bge.logic.keyboard.events)
    xpos = own['prevPos'][0]
    ypos = own['prevPos'][1]
    own['prevPos'] = mouse.position
    if keys[bge.events.MIDDLEMOUSE] == bge.logic.KX_INPUT_NONE:
        x = INV_X*(xpos - mouse.position[0])
        y = -INV_Y*(ypos - mouse.position[1])
        x *= SENSITIVITY
        y *= SENSITIVITY
        #bge.render.setMousePosition(xpos,ypos)
    elif keys[bge.events.MIDDLEMOUSE] == bge.logic.KX_INPUT_JUST_RELEASED:
        #bge.render.setMousePosition(xpos,ypos)
        x = y = 0
    else:
        x = y = 0
    
    # smooth movement
    own['oldx'] = (own['oldx']*S + x*(1-S))
    own['oldy'] = (own['oldy']*S + y*(1-S))
    x = own['oldx']
    y = own['oldy']
    
    # zoom controls
    if keys[ZoomIn] and own['zoom'] >= MIN_DIST + ZOOM_STEP:
        own['zoom'] -= ZOOM_STEP
    elif keys[ZoomOut] and own['zoom'] <= MAX_DIST - ZOOM_STEP:
        own['zoom'] += ZOOM_STEP
        
    # update angles
    inv = False
    own['x'] += x
    own['y'] -= y
    
    if own['x'] > math.pi:
        own['x'] -= 2*math.pi
        inv = True
    elif own['x'] <= -2*math.pi:
        own['x'] += 2*math.pi
        inv = True
    if own['y'] > math.pi - 2*MIN_ANGLE:
        own['y'] = math.pi - 2*MIN_ANGLE
    elif own['y'] < 0.0 + MIN_ANGLE:
        own['y'] = 0.0 + MIN_ANGLE
    
    # LR toggling and smoothing
    if keys[ToggleLR] == 1:
        own['LRinv'] *= -1
    dist = own['LRinv']*HOR_OFFSET - own['LR']
    own['LR'] += dist / LRSMOOTH
    dist = own['LR'] / math.pow(own['dist'], 2)
    
    # calculate and set new camera position
    own['tdist'] = own['dist']*math.fabs(math.cos(math.pi/2 - own['y']))
    zshift = own['dist']*math.sin(math.pi/2 - own['y'])
    z = player.worldPosition[2] + zshift + Z_OFFSET
    yshift = own['tdist']*-math.cos(own['x'])
    y = player.worldPosition[1] + yshift + dist*math.sin(own['x'])
    xshift = own['tdist']*math.sin(own['x'])
    x = player.worldPosition[0] + xshift + dist*math.cos(own['x'])
    own.worldPosition = [x,y,z]
    
    #set new camera angle
    vec = mathu.Vector((own['y'], 0, own['x']))
    own.localOrientation = vec

As for the collision issue, I can’t really tell what the problem is from just the video. You should try to create a simple blend file that replicates the problem and submit that.

Great work! you just keep helping people since 2012, very useful and clean, thanks a lot.