Obstacle avoidance:
sync on
sync rate 20
autocam off
randomize timer()
d3d_init
set camera view 0, 0, 1024, 768
d3d_setview 0, 0, 1024, 768
d3d_color 255, 0, 255, 100
Rem PJY - the idea behind this example programme is that the green cube is "wandering"
Rem PJY - there are however lots of purple spheres which act AS obstacles.
Rem PJY - the cube tries to avoid these by applying a suitable steering force
TYPE vec
x AS float
y AS float
z AS float
ENDTYPE
TYPE obj
current_pos AS vec
target_pos AS vec
heading_vec AS vec
desired_vec AS vec
targ_vec AS vec
side_vec AS vec
velocity AS vec
current_speed AS float
max_speed AS float
max_force AS float
targ_distance AS float
mass AS integer
max_force AS double integer
max_turn_force AS double integer
wander_radius AS float
wander_distance AS float
wander_jitter AS float
wander_target AS vec
bounding_radius AS integer
box_length AS float
ENDTYPE
TYPE obstacle
current_pos AS vec
obj_no AS integer
radius AS integer
ENDTYPE
global camera AS vec
global camera_distance AS float
global camera_azimuth AS float
global camera_bearing AS float
camera_distance = 100
camera_azimuth = 70
camera_bearing = 30
global wanderer = 1
dim ship(1) AS obj
Rem PJY - lots of standard stuff to set up the green cube
ship(1).current_pos.x = -10
ship(1).current_pos.y = 5
ship(1).current_pos.z = -10
ship(1).velocity.x = 0
ship(1).velocity.y = 0
ship(1).velocity.z = 0
ship(1).current_speed = 0
ship(1).max_speed = 10
ship(1).max_force = 30
ship(1).mass = 10
ship(1).wander_radius = 3
ship(1).wander_distance = 10
ship(1).wander_jitter = 0.5
ship(1).wander_target.x = ship(1).current_pos.x
ship(1).wander_target.y = ship(1).current_pos.y
ship(1).wander_target.z = ship(1).current_pos.z
ship(1).bounding_radius = 6
Rem PJY - create 20 purple spheres AS obstacles
global gTotal_obstacles = 30
dim obstacles(gTotal_obstacles) AS obstacle
for i = 1 to gTotal_obstacles
obstacles(i).current_pos.x = (rnd(400)) - 200
obstacles(i).current_pos.y = (rnd(400)) - 200
obstacles(i).current_pos.z = (rnd(400)) - 200
obstacles(i).obj_no = i + 9
obstacles(i).radius = 20
make object sphere obstacles(i).obj_no, obstacles(i).radius * 2, 15, 15
position object obstacles(i).obj_no, obstacles(i).current_pos.x, obstacles(i).current_pos.y, obstacles(i).current_pos.z
color object obstacles(i).obj_no, rgb(160, 032, 240)
next i
Rem PJY - create a matrix just to act AS a reference for the human eye
make matrix 1, 1000, 1000, 100, 100
position matrix 1, -500, 0, -500
set matrix 1, 1, 0, 1, 1, 0, 1, 0
Rem PJY - cube 1 will be the wanderer
make object cube 1, 10
position object 1, ship(1).current_pos.x, ship(1).current_pos.y, ship(1).current_pos.z
color object 1, rgb(0, 255, 0)
Rem PJY - sphere 2 has the same role AS in the "wandering" example, i.e. it
Rem PJY - demonstrates movement
make object sphere 2, 8
position object 2, ship(1).target_pos.x, ship(1).target_pos.y, ship(1).target_pos.z
color object 2, rgb(0, 244, 0)
Rem PJY - create a wireframe long rectangle thing to demonstrate the box's obstacle collision area
make object cube 3, 10
set object wireframe 3, 1
position camera 0, ship(1).current_pos.x - 50, 100, ship(1).current_pos.z - 50
point camera 0, ship(1).current_pos.x, ship(1).current_pos.y, ship(1).current_pos.z
Rem PJY - create a chase camera
make camera 1
set camera view 1, 824, 0, 1024, 200
set current camera 0
current_time = timer()
repeat
Rem PJY - safety code to ensure that the wandering cube hasn't "wandered off" hehe
if ship(wanderer).current_pos.x > 450 then ship(wanderer).current_pos.x = -400
if ship(wanderer).current_pos.x < -450 then ship(wanderer).current_pos.x = 400
if ship(wanderer).current_pos.y > 450 then ship(wanderer).current_pos.y = -400
if ship(wanderer).current_pos.y < -450 then ship(wanderer).current_pos.y = 400
if ship(wanderer).current_pos.z > 450 then ship(wanderer).current_pos.z = -400
if ship(wanderer).current_pos.z < -450 then ship(wanderer).current_pos.z = 400
Rem PJY - a bit of info
text 0, 0, "Current_pos : " + str$(ship(wanderer).current_pos.x) + " " + str$(ship(wanderer).current_pos.y) + " " + str$(ship(wanderer).current_pos.z)
text 0, 30, "Velocity vec: " + str$(ship(wanderer).velocity.x) + " " + str$(ship(wanderer).velocity.y) + " " + str$(ship(wanderer).velocity.z)
Rem PJY - now what happens is that you ADD TOGETHER the steering forces
Rem PJY - generated by wandering AND obstacle avoidance (if any)
behaviour_wander(wanderer)
behaviour_obstacle_avoidance(wanderer)
Rem PJY - update the time
last_time = current_time
current_time = timer()
time_elapsed# = current_time - last_time
time_elapsed# = time_elapsed# * 0.01
Rem PJY - update the vehicle (the green cube)
update_vehicle(time_elapsed#, wanderer)
position object 1, ship(wanderer).current_pos.x, ship(wanderer).current_pos.y, ship(wanderer).current_pos.z
position object 2, ship(wanderer).target_pos.x, ship(wanderer).target_pos.y, ship(wanderer).target_pos.z
Rem PJY - keep camera aimed at the wandering green cube
control_camera()
position camera 0, camera.x, camera.y, camera.z
point camera 0, ship(wanderer).current_pos.x, ship(wanderer).current_pos.y, ship(wanderer).current_pos.z
Rem PJY - position the chase camera 1
center text 924, 0, "Chase camera"
EZro_SetEuler object angle x(wanderer), object angle y(wanderer), object angle z(wanderer)
EZro_SetPos ship(wanderer).current_pos.x, ship(wanderer).current_pos.y, ship(wanderer).current_pos.z
EZro_FindPointFromOffset -30, 15, 0
position camera 1, EZro_GetOffsetX(), EZro_GetOffsetY(), EZro_GetOffsetZ()
point camera 1, ship(wanderer).current_pos.x, ship(wanderer).current_pos.y, ship(wanderer).current_pos.z
sync
until spacekey() > 0
Rem PJY - a bit of clean up
for i = 1 to 200
if matrix exist(i) = 1
delete matrix i
endif
if object exist(i) = 1
delete object i
endif
next i
Rem PJY - control camera using spherical coordinates
function control_camera()
if upkey() > 0
camera_azimuth = camera_azimuth + 2
if camera_azimuth => 180
camera_azimuth = 180
endif
endif
if downkey() > 0
camera_azimuth = camera_azimuth - 2
if camera_azimuth =< 0
camera_azimuth = 0
endif
endif
if rightkey() > 0
camera_bearing = wrapvalue(camera_bearing + 2)
endif
if leftkey() > 0
camera_bearing = wrapvalue(camera_bearing - 2)
endif
Rem PJY - use spherical 3d coordinates to calculate the camera's new position
camera.x = ship(wanderer).current_pos.x + (camera_distance * sin(camera_azimuth) * cos(camera_bearing))
camera.z = ship(wanderer).current_pos.z + (camera_distance * sin(camera_azimuth) * sin(camera_bearing))
camera.y = ship(wanderer).current_pos.y + (camera_distance * cos(camera_azimuth))
endfunction
Rem PJY - function that updates the ship's behaviour
function behaviour_wander(obj_no AS integer)
null = make vector3(1)
null = make vector3(2)
null = make vector3(3)
null = make vector3(4)
random1# = random_clamped()
random2# = random_clamped()
random3# = random_clamped()
set vector3 1, random1# * ship(obj_no).wander_jitter, random2# * ship(obj_no).wander_jitter, random3# * ship(obj_no).wander_jitter
ship(obj_no).wander_target.x = ship(obj_no).wander_target.x + x vector3(1)
ship(obj_no).wander_target.y = ship(obj_no).wander_target.y + y vector3(1)
ship(obj_no).wander_target.z = ship(obj_no).wander_target.z + z vector3(1)
set vector3 2, ship(obj_no).wander_target.x, ship(obj_no).wander_target.y, ship(obj_no).wander_target.z
normalize vector3 2, 2
multiply vector3 2, ship(obj_no).wander_radius
ship(obj_no).wander_target.x = x vector3(2)
ship(obj_no).wander_target.y = y vector3(2)
ship(obj_no).wander_target.z = z vector3(2)
targetlocal AS vec
targetlocal.x = ship(obj_no).wander_target.x * ship(obj_no).wander_distance
targetlocal.y = ship(obj_no).wander_target.y * ship(obj_no).wander_distance
targetlocal.z = ship(obj_no).wander_target.z * ship(obj_no).wander_distance
targetworld AS vec
set vector3 1, targetlocal.x, targetlocal.y, targetlocal.z
set vector3 2, ship(obj_no).current_pos.x, ship(obj_no).current_pos.y, ship(obj_no).current_pos.z
set vector3 3, ship(obj_no).heading_vec.x, ship(obj_no).heading_vec.y, ship(obj_no).heading_vec.z
set vector3 4, ship(obj_no).side_vec.x, ship(obj_no).side_vec.y, ship(obj_no).side_vec.z
add vector3 1, 1, 2
add vector3 1, 1, 3
add vector3 1, 1, 4
ship(obj_no).target_pos.x = x vector3(1)
ship(obj_no).target_pos.y = y vector3(1)
ship(obj_no).target_pos.z = z vector3(1)
subtract vector3 1, 1, 2
ship(obj_no).desired_vec.x = x vector3(1)
ship(obj_no).desired_vec.y = y vector3(1)
ship(obj_no).desired_vec.z = z vector3(1)
null = delete vector3(1)
null = delete vector3(2)
null = delete vector3(3)
null = delete vector3(4)
endfunction
Rem PJY - function that updates the ship's behaviour
function behaviour_seek(obj_no AS integer)
null = make vector3(1)
null = make vector3(2)
null = make vector3(3)
Rem PJY - get the new vector to target
set vector3 1, ship(obj_no).target_pos.x - ship(obj_no).current_pos.x, ship(obj_no).target_pos.y - ship(obj_no).current_pos.y, ship(obj_no).target_pos.z - ship(obj_no).current_pos.z
Rem PJY - check its not further than the maximum speed of the cube
ship(obj_no).targ_distance = length vector3(1)
normalize vector3 1, 1
multiply vector3 1, ship(obj_no).max_speed
ship(obj_no).targ_vec.x = x vector3(1)
ship(obj_no).targ_vec.y = y vector3(1)
ship(obj_no).targ_vec.z = z vector3(1)
Rem PJY - subtract desired velocity from current velocity
set vector3 2, ship(obj_no).velocity.x, ship(obj_no).velocity.y, ship(obj_no).velocity.z
subtract vector3 1, 1, 2
ship(obj_no).desired_vec.x = x vector3(1)
ship(obj_no).desired_vec.y = y vector3(1)
ship(obj_no).desired_vec.z = z vector3(1)
null = delete vector3(1)
null = delete vector3(2)
null = delete vector3(3)
endfunction
Rem PJY - function that updates the ship's behaviour
Rem PJY - this function ASsumes the ship's facing in object space is the X axis
function behaviour_obstacle_avoidance(obj_no AS integer)
Rem PJY - calculate the "box" collision area of our wandering cube
Rem PJY - 5 just acts as a minimum detection length
ship(obj_no).box_length = 6 + (ship(obj_no).current_speed / ship(obj_no).max_speed * 6)
scale object 3, 100 + (100 * ship(obj_no).box_length), 100, 100
Rem PJY - tag obstacles close to the box
Rem PJY - we won't do this step in this example but you would have to
Rem PJY - in a large and complex world
dist_to_closest_obstacle AS float
dist_to_closest_obstacle = 1000000
closest_obstacle AS integer
closest_obstacle = 0
closest_obstacle_pos AS vec
Rem PJY - iterate through the obstacles and find the closest obstacle
for i = 1 to gTotal_obstacles
Rem PJY - reset colour of obstacles to purple
color object obstacles(i).obj_no, rgb(160, 032, 240)
Rem PJY - here you should normally test whether obstacle is within range
Rem PJY - i.e. do a distance test
Rem PJY - but because I'm not dealing with some kind of horrific game world, just 20 purple spheres,
Rem PJY - I'm not going to bother.
Rem PJY - now onto something more interesting
Rem PJY - transform obstacle into the wanderer's object space (Mike, I WISH we had a native DBPro command that did this easily hint hint)
Rem PJY - set eZrotate up with wanderer's current rotation
EZro_SetEuler object angle x(obj_no), object angle y(obj_no), object angle z(obj_no)
Rem PJY - set EZrotate up with wanderer's current position
EZro_SetPos ship(obj_no).current_pos.x, ship(obj_no).current_pos.y, ship(obj_no).current_pos.z
Rem PJY - calculate offsets to the wanderer's local space
EZro_FindoffsetFrompoint obstacles(i).current_pos.x, obstacles(i).current_pos.y, obstacles(i).current_pos.z
Rem PJY - put them into variables
localpos AS vec
localpos.x = EZro_GetOffsetX()
localpos.y = EZro_GetOffsetY()
localpos.z = EZro_GetOffsetZ()
Rem PJY - ignore objects that lay behind the wanderer
Rem PJY - btw this assumes that the object's default facing is along the X axis
Rem PJY - actually in most games its actually along the Z axis
Rem PJY - but I'm just a v. difficult person
if localpos.x => 0
expanded_radius AS float
expanded_radius = (obstacles(i).radius + ship(obj_no).bounding_radius)
if (abs(localpos.y) < expanded_radius) AND (abs(localpos.z) < expanded_radius)
Rem PJY - now do a line/circle intersection test
cX AS float
cY AS float
cZ AS float
cX = localpos.x
cY = localpos.y
cZ = localpos.z
distance# = (cX * cX) + (cY * cY) + (cZ * cZ)
if distance# < dist_to_closest_obstacle
dist_to_closest_obstacle = distance#
closest_obstacle = i
closest_obstacle_pos = localpos
endif
endif
endif
next i
Rem PJY - if there is a closest obstacle, calculate a steering force
steering_force AS vec
temp_vec AS vec
if closest_obstacle > 0
Rem PJY - colour the closest obstacle YELLOW
color object obstacles(closest_obstacle).obj_no, RGB(255,255,000)
text 0, 90, "Closest test active"
multiplier AS float
multiplier = 2.0 + (box_length - localpos.x) / box_length
steering_force.y = (obstacles(closest_obstacle).radius - localpos.y) * multiplier
steering_force.z = (obstacles(closest_obstacle).radius - localpos.z) * multiplier
braking_weight AS float
braking_weight = 0.2
steering_force.x = (obstacles(closest_obstacle).radius - localpos.x) * braking_weight
Rem PJY - now recalculate the steering force vector into world space
Rem PJY - setup EZrotate with the object's current rotation
EZro_SetEuler object angle x(obj_no), object angle y(obj_no), object angle z(obj_no)
Rem PJY - setup EZrotate with the object's current position
EZro_SetPos ship(obj_no).current_pos.x, ship(obj_no).current_pos.y, ship(obj_no).current_pos.z
Rem PJY - find the new vector from the steering force
EZro_FindPointFromOffset steering_force.x, steering_force.y, steering_force.z
temp_vec.x = EZro_GetOffsetX()
temp_vec.y = EZro_GetOffsetY()
temp_vec.z = EZro_GetOffsetZ()
endif
Rem PJY - insert into the desired vector
ship(obj_no).desired_vec.x = ship(obj_no).desired_vec.x + temp_vec.x
ship(obj_no).desired_vec.y = ship(obj_no).desired_vec.y + temp_vec.y
ship(obj_no).desired_vec.z = ship(obj_no).desired_vec.z + temp_vec.z
endfunction
Rem PJY - function to update the vehicle's physics
function update_vehicle(time_elapsed AS float, obj_no AS integer)
null = make vector3(1)
null = make vector3(2)
null = make vector3(3)
null = make vector3(4)
steering_force AS vec
steering_force.x = ship(obj_no).desired_vec.x
steering_force.y = ship(obj_no).desired_vec.y
steering_force.z = ship(obj_no).desired_vec.z
set vector3 1, steering_force.x, steering_force.y, steering_force.z
length# = length vector3(1)
if length# > ship(obj_no).max_force
normalize vector3 1, 1
multiply vector3 1, ship(obj_no).max_force
endif
steering_force.x = x vector3(1)
steering_force.y = y vector3(1)
steering_force.z = z vector3(1)
acceleration AS vec
acceleration.x = steering_force.x
acceleration.y = steering_force.y
acceleration.z = steering_force.z
set vector3 1, acceleration.x, acceleration.y, acceleration.z
divide vector3 1, ship(obj_no).mass
acceleration.x = x vector3(1)
acceleration.y = y vector3(1)
acceleration.z = z vector3(1)
set vector3 1, acceleration.x, acceleration.y, acceleration.z
multiply vector3 1, time_elapsed
ship(obj_no).velocity.x = ship(obj_no).velocity.x + x vector3(1)
ship(obj_no).velocity.y = ship(obj_no).velocity.y + y vector3(1)
ship(obj_no).velocity.z = ship(obj_no).velocity.z + z vector3(1)
set vector3 1, ship(obj_no).velocity.x, ship(obj_no).velocity.y, ship(obj_no).velocity.z
length# = length vector3(1)
if length# => ship(obj_no).max_speed
normalize vector3 1, 1
multiply vector3 1, ship(obj_no).max_speed
endif
ship(obj_no).velocity.x = x vector3(1)
ship(obj_no).velocity.y = y vector3(1)
ship(obj_no).velocity.z = z vector3(1)
ship(obj_no).current_speed = length vector3(1)
multiply vector3 1, time_elapsed
ship(obj_no).current_pos.x = ship(obj_no).current_pos.x + x vector3(1)
ship(obj_no).current_pos.y = ship(obj_no).current_pos.y + y vector3(1)
ship(obj_no).current_pos.z = ship(obj_no).current_pos.z + z vector3(1)
old_heading AS vec
old_heading.x = ship(obj_no).heading_vec.x
old_heading.y = ship(obj_no).heading_vec.y
old_heading.z = ship(obj_no).heading_vec.z
Rem PJY - create a new heading vector
set vector3 1, ship(obj_no).velocity.x, ship(obj_no).velocity.y, ship(obj_no).velocity.z
normalize vector3 1, 1
ship(obj_no).heading_vec.x = x vector3(1)
ship(obj_no).heading_vec.y = y vector3(1)
ship(obj_no).heading_vec.z = z vector3(1)
Rem PJY - point the wandering cube towards that new heading
Rem PJY - using its local X axis
Rem PJY - first calculate the angle between the cube and the new heading
EZro_SetEuler object angle x(obj_no), object angle y(obj_no), object angle z(obj_no)
set vector3 2, EZro_GetMatxX(), EZro_GetMatxY(), EZro_GetMatxZ()
set vector3 3, ship(obj_no).heading_vec.x, ship(obj_no).heading_vec.y, ship(obj_no).heading_vec.z
angle# = acos(dot product vector3(3, 2))
Rem PJY - next calculate the new heading location
set vector3 4, ship(obj_no).current_pos.x, ship(obj_no).current_pos.y, ship(obj_no).current_pos.z
add vector3 3, 3, 4
Rem PJY - now use EZrotate's rotateto command to rotate the cube to the new heading
EZro_SetEuler object angle x(obj_no), object angle y(obj_no), object angle z(obj_no)
EZro_SetPos ship(obj_no).current_pos.x, ship(obj_no).current_pos.y, ship(obj_no).current_pos.z
EZRo_rotateto 0, x vector3(3), y vector3(3), z vector3(3), angle#
EZro_FindEuler
rotate object obj_no, EZro_GetEulerX(), EZro_GetEulerY(), EZro_GetEulerZ()
Rem PJY - create a blue line to demonstrate the heading of the wandering cube
temp_vec AS vec
temp_vec.x = ship(obj_no).heading_vec.x * 15
temp_vec.y = ship(obj_no).heading_vec.y * 15
temp_vec.z = ship(obj_no).heading_vec.z * 15
d3d_line3d ship(obj_no).current_pos.x, ship(obj_no).current_pos.y, ship(obj_no).current_pos.z, ship(obj_no).current_pos.x + temp_vec.x, ship(obj_no).current_pos.y + temp_vec.y, ship(obj_no).current_pos.z + temp_vec.z, 1
Rem PJY - reposition the collision box (shouldn't put this code here as it'd look better
Rem PJY - in the main loop. But I'm lazy. C'est la vie)
set object to object orientation 3, 1
set vector3 3, ship(obj_no).heading_vec.x, ship(obj_no).heading_vec.y, ship(obj_no).heading_vec.z
multiply vector3 3, ship(obj_no).box_length * 5
add vector3 3, 3, 4
position object 3, x vector3(3), y vector3(3), z vector3(3)
Rem PJY - create a new perpendicular vector that represents the side
set vector3 2, 0, 0, 0
cross product vector3 1, 1, 2
ship(obj_no).side_vec.x = x vector3(1)
ship(obj_no).side_vec.y = y vector3(1)
ship(obj_no).side_vec.z = z vector3(1)
null = delete vector3(1)
null = delete vector3(2)
null = delete vector3(3)
null = delete vector3(4)
endfunction
Rem PJY - returns a value between -1 and 1
function random_clamped()
local temp#
temp# = rnd(200)
temp# = temp# - 100
temp# = temp# * 0.01
endfunction temp#
Incidentally, you'll see that the cube "detects" various obstacles that don't not apparently fall within its detection box. This is because, as you'll see from my maths above, that the actual detection box is bigger than that represented on screen. The reason for this is that there is a margin for error.
Plus, sometimes the cube doesn't avoid a particular obstacle. This is because of my relatively weak settings for the obstacle avoidance force. A bit of playing with those variables produces a huge range of results from massively paranoid obstacle avoidance to shaving the sides off the obstacle.
Cheer if you like bears! Cheer if you like jam sandwiches!
RiiDii: "I highly recommend Philip's vector tutorials"
P3.2ghz / 1 gig / GeForce FX 5900 128meg / WinXP home