I know that KidsCanCode.com has a tutorial on context-based steering but, it's focuses on 2d and I don't know how to alter the code so it can work in 3d. I've watched the YouTube video on the subject , so I know that it can work in 3d( even though the video didn't show examples of the 3d code). Does anyone know how to make the code work in 3d? GDScript had a video on a simple and equally important topic entitled The Easy Way to Make Enemies See in Godot; video also had similar looking code. However, I don't know how to make the code in that video work in 3d. Does anyone know how to make the code in these videos work in 3d? Every time I try to follow the tutorial in 3d, I change the 2d raycast to a 3d raycast and the Vector 2 to Vector 3. However, the code doesn't seem to work at all. Does anyone know how I can adapt the code in these tutorials in 3d? It would be great if I could use this technique for an enemy AI I'm working on.

I was interested in it also, so I'm kind of looking at it. What I did so far is to add 8 raycasts and cast them in that half circle pattern using his code. There's a lot to understand in that program and I'm not that good at trig and stuff. I checked collisions in debug so I could see the raycasts. I'll be a while at it. Someone else could probably do it in a few minutes. This is what I have so far:

@fire7side said: I was interested in it also, so I'm kind of looking at it. What I did so far is to add 8 raycasts and cast them in that half circle pattern using his code. There's a lot to understand in that program and I'm not that good at trig and stuff. I checked collisions in debug so I could see the raycasts. I'll be a while at it. Someone else could probably do it in a few minutes. This is what I have so far:

Thanks. I'm starting to get some ideas. A line of code that was giving me problems was this:

ray.cast_to. = Vector3.UP.rotated(angle)* max_view_distance

In the tutorials, this could would have had a Vector2 instead of a Vector3. So when I turned the Vector2 into a Vector3, the rotate function gave me an error demanding an additional argument and I got another error when I add a float as another argument. After some experimenting, I know that the arrays are being added, it's just that I have to find a way to rotate them probably. So here's my plan, I'm just going to make a variable with a number which I want to rotate a raycast and then use it in the loop. However, I'll add more to that number, every time the loop repeats itself. I'm too tired to try it now but, I'll do it tomorrow and then share if it was successful.

This is what I did on it.

func _ready():
	interest.resize(num_rays)
	danger.resize(num_rays)
	ray_directions.resize(num_rays)
	var myvec = Vector3.RIGHT
	for i in num_rays:
		var angle = i * PI /(num_rays -1)
		ray_directions[i] = myvec.rotated(Vector3.UP,angle)

I didn't test it on anything but 8. His code was made for a full circle, but that doesn't really make sense to me. That's not what it showed on his diagram.

I don't know if you got it working or not. I think this the key to it. It's the set default interest which is when the object is pointing right at the target.

func set_default_interest():
	for i in num_rays:
		var d = ray_directions[i].dot(transform.basis.z)
		interest[i] = min(0, d)
		print(d)

It prints out:

0
-0.433884
-0.781832
-0.974928
-0.974928
-0.781832
-0.433884
0

since it goes around from right to left, the two center ones have the most interest, although absolute value might be needed because it travels in the -z direction.

@fire7side

Okay, so I'm up and I'm finally focused. So I tried my suggested solution( for adding and rotating the rays) and it worked. However, I didn't get what you have with admittedly has a cooler looking end result. Check it out:

func raycast_generator():
	var ray_count = angle_cone_of_vision / angle_between_raycast 
	for index in ray_count:
		var ray = RayCast.new() 
		var angle = 0
		ray.cast_to.z = -100
		if angle < angle + angle_index:
			angle = angle + angle_index
			angle_index += angle_index # angle_index is orignally 10
		if ray.rotation.y != angle:
			ray.rotation.y = angle
		ray.enabled = true
		add_child(ray)

Admittedly, what I have here does have some problems but the rays have been created and they've be rotated around the character and away from each other( though I suspect that some are still on top of each other). I'm glad that I came up with something I understand for myself. Though I haven't completely explored your solution yet. You can tell me what you think of what I've got going on here and how it can be improved.

Edit: Here is a screenshot of what it looks like in action:

Edit again:

Okay, so after looking back on your code. I remember I forgot about the danger and interest arrays.

Okay, so after looking at your code. I made a little alteration to my code to make it looker cleaner, while get the same result I had previously.

func raycast_generator():
	var ray_count = angle_cone_of_vision / angle_between_raycast 
	for index in ray_count:
		var ray = RayCast.new() 
		var angle = 0
		ray.cast_to.z = -100
		ray.rotation.y = 10 * index
		ray.enabled = true
		add_child(ray)

I do suspect my code this has the other problem I've mentioned before.

It's pretty important that the rays are evenly spaced for the steering, I think. Not positive on that. Your world looks definitely better than mine. Pretty cool. I noticed in his code he didn't factor in the distance when something was hit. At least I didn't see it. I guess I'll try the same thing at first. I'm not sure if rotation is in angles or radians but I think it's in radians which means that thing is really winding around. You could try deg_to_rad(360/8) and see what it does.

@fire7side said: You could try deg_to_rad(360/8) and see what it does.

When I tried that number in Deg2Rad, all the arrays stacked on each other. When I did it without Ded2Rad, I got a crazy amount of rays. If my maths is correct, I think I got 45 rays:

I feel that we're on to sometime. I don't think we should get distracted by trying to come up with the simplest solutions but, it would be funny if we came up with a solution that is similar that what KidsCanCode intended.

Edit: I wonder what would happen if I were to just turn rotation_degrees into just rotation. Hmm. I'll be right back.

Edit again: Oh way, I already had rotation instead of rotation degrees.

Another edit: Okay, so after turning the ray_count back to angle_cone_of_vision / angle_between_raycast and turning the rotation to rotation_degrees. I got sometime that looked like this:

Edit: Hmm...I probably wasn't following your instructions properly.

Okay, so I followed your suggestion properly( I think). Here is what I got:

and here is what my code looks like:

func raycast_generator():
	var ray_count = 8
	for index in ray_count:
		var ray = RayCast.new() 
		var angle = 0
		ray.cast_to.z = -20
		ray.rotation.y = deg2rad(360/8) * index
		ray.enabled = true
		add_child(ray)

Edit: All of the rays seems equally spaced out.

That's good, but you need to save the direction vector for later. I don't know if you declared ray_directions in your code like his. Then I think you would do ray_directions[index] = ray.normalized. That's if ray is a vector 3. Somewhere after it's all done with the rotation. If it's not a vector3, which it might not be, then I don't know what you do. Maybe there's some way of getting it from raycast.

@fire7side

So my attempt to adding the directions to the ray_directions array is based on what you've got and what he's got. Here is what it looks like:

func raycast_generator():
	ray_direction.resize(ray_num)
	var vec = Vector3.RIGHT
	for index in ray_num:
		var ray = RayCast.new() 
		ray.cast_to.z = -20
		ray.rotation.y = deg2rad(360/ray_num) * index
		ray.enabled = true
		add_child(ray)
		ray_direction[index] = vec.rotated(Vector3.UP,deg2rad(360/ray_num) * index)
		print(ray_direction[index])

I've managed to get rid of the ray_count variable as it was taking up unnecessary space. However, some sort of ray_num variable has returned for convinence. Here is what was printed in the the output:


(1, 0, 0)
(0.707107, 0, -0.707107)
(-0, 0, -1)
(-0.707107, 0, -0.707107)
(-1, 0, 0)
(-0.707107, 0, 0.707107)
(0, 0, 1)
(0.707107, 0, 0.707107)

It looks like I did get vectors into the array but, I'm not completely sure if it's the correct location. What do you think?

I think it's right. You could maybe draw lines or raycasts from 0,0,0 to those points to see. Direction vectors don't show location, they only show direction. Every reading has a zero in the y location, so it's a flat disc.

@ I'll admit that I don't know how to check this with code. I thought it would be easy as finding some sort of direction property off the raycast but, I was wrong. I'm a bit tired now. If I still have access to the internet tomorrow, I'll be back then.

I'm pretty sure it's right. I ended up having to make a circular pattern also. I have it working in default mode where it wanders around and turns when it gets near something. This is my code so far:

extends KinematicBody

export var max_speed = 350
export var steer_force = 0.1
export var look_ahead = 4
export var num_rays = 8

# context array
var ray_directions = []
var interest = []
var danger = []

var chosen_dir = Vector3.ZERO
var velocity = Vector3.ZERO
var acceleration = Vector3.ZERO
var raycasts = []
var last_dir = Vector3.ZERO

# Called when the node enters the scene tree for the first time.
func _ready():
	ray_directions.resize(num_rays)
	interest.resize(num_rays)
	danger.resize(num_rays)
	var myvec = Vector3.RIGHT
	for i in num_rays:
		var angle = i * 2*PI /(num_rays)
		ray_directions[i] = myvec.rotated(Vector3.UP,angle)
	raycasts = $rayFront.get_children()
	var i = 0
	for ray in raycasts:
		ray.set_cast_to(ray_directions[i]*look_ahead)
		ray.add_exception(get_node("player"))
		i+=1


func _physics_process(delta):
	set_interest()
	set_danger()
	choose_direction()
	var rotAngle = .1

	chosen_dir.x = lerp(last_dir.x,chosen_dir.x,.03)
	chosen_dir.z = lerp(last_dir.z,chosen_dir.z,.03)
	look_at(Vector3(chosen_dir.x+translation.x,translation.y,chosen_dir.z+translation.z),Vector3.UP)
	move_and_collide(chosen_dir * delta * 2)
	last_dir = chosen_dir
	
func set_interest():
	set_default_interest()
	
func set_default_interest():
	for i in num_rays:
		var d = ray_directions[i].dot(transform.basis.z)
		interest[i] = min(0, d)
		#print(d)
	
func set_danger():
	var i = 0
	for ray in raycasts:
		if ray.is_colliding():
			danger[i] = 1
			if i < 4:
				interest[i+4]+= -1
			else: interest[i-4]+= -1

		else: danger[i] = 0
		i+= 1
	
func choose_direction():
	# Eliminate interest in slots with danger
	for i in num_rays:
		if danger[i] > 0.5:
			interest[i] = 0.0
	# Choose direction based on remaining interest
	chosen_dir = Vector3.ZERO
	for i in num_rays:
		chosen_dir += ray_directions[i] * abs(interest[i])
	chosen_dir = chosen_dir.normalized()

@fire7side

So I was wanted to try understanding your code, before attempting something on my own. However, it just seems that your code works the best. So I just deceived to keep my code for the raycast_generator function but, borrow everything else you've got and I edited to avoid any errors that would arise. When I ran the game, I noticed the ai started moving. However, despite it's attempt and trying to avoid obstacles, it kept hitting into things and getting such. Still, I'm glad that it's at least trying to avoid obstacles.

@fire7side

Okay, so I think my AI is actually moving towards anything the raycast touches as oppose to away from it.

It was working for me. I did some more improvements that might help. It moves toward a target now.

It played for me with the youtube link but doesn't in this window for some reason.

Here's the code. Not sure if I'll do more on it or not. Hope you get it working. A look_ahead of 5 worked the best for me.

xtends KinematicBody

export var max_speed = 3
export var steer_force = 0.5
export var look_ahead = 4
export var num_rays = 8
onready var target_position = get_parent().get_target_position()
var stop = true

# context array
var ray_directions = []
var interest = []
var danger = []

var chosen_dir = Vector3.ZERO
var velocity = Vector3.ZERO
var acceleration = Vector3.ZERO
var raycasts = []
var last_dir = Vector3.ZERO

# Called when the node enters the scene tree for the first time.
func _ready():
	target_position = get_parent().get_target_position()
	ray_directions.resize(num_rays)
	interest.resize(num_rays)
	danger.resize(num_rays)
	var myvec = Vector3.RIGHT
	for i in num_rays:
		var angle = i * 2*PI /(num_rays)
		ray_directions[i] = myvec.rotated(Vector3.UP,angle)
	raycasts = $rayFront.get_children()
	var i = 0
	for ray in raycasts:
		ray.set_cast_to(ray_directions[i]*look_ahead)
		ray.add_exception(get_parent())
		i+=1
func _unhandled_input(event):
	if event is InputEventKey:
		if event.pressed and event.scancode == KEY_0:
			stop = false

func _physics_process(delta):
	set_interest()
	set_danger()
	choose_direction()

	chosen_dir.x = lerp(last_dir.x,chosen_dir.x,.01)
	chosen_dir.z = lerp(last_dir.z,chosen_dir.z,.01)
	look_at(Vector3(chosen_dir.x+translation.x,translation.y,chosen_dir.z+translation.z),Vector3.UP)
	if !stop:
		move_and_collide(chosen_dir * delta * max_speed)
		last_dir = chosen_dir
	
	
func set_interest():
	if target_position == null:
		set_default_interest()
	else:
			var direction = (translation - target_position).normalized()
			for i in num_rays:
				var d = ray_directions[i].dot(direction)* steer_force
				interest[i] = min(0, d)
				if(translation - target_position).length() < 1:
					stop = true

		
	
func set_default_interest():
	for i in num_rays:
		var d = ray_directions[i].dot(transform.basis.z)
		interest[i] = min(0, d)
		#print(d)
	
func set_danger():
	var i = 0
	for ray in raycasts:
		if ray.is_colliding():
			danger[i] = 1
			if i < 4:
				interest[i+4]+= -1
			elif i > 3: interest[i-4]+= -1

		else: danger[i] = 0
		i+= 1
	
func choose_direction():
	# Eliminate interest in slots with danger
	for i in num_rays:
		if danger[i] > 0.5:
			interest[i] = 0.0
	# Choose direction based on remaining interest
	chosen_dir = Vector3.ZERO
	for i in num_rays:
		chosen_dir += ray_directions[i] * abs(interest[i])
	chosen_dir = chosen_dir.normalized()

@fire7side

That looks cool. Even though I don’t think there is nothing wrong with your code, I have a hard time understanding it and therefore I can’t make changes to it that would help me. It’s a possibility that I could have placed the wrong directions in the direction variables. Still, I feel like I'll have a hard time solving this problem in the future, if it were to arise again, if I don't know what I'm doing now. I have other questions about your code but, for now, could you test the code for my raycast_generator on your end to so what could be going wrong?

I'll try to take a look like tomorrow or something. I'm a little bit burned out on it.

@fire7side

Hi, it's me again. So I was working on an alternative solution that is incredibly simple, noob friendly and only required one ray cast( So it's less taxing on the hardware). Here is what came up with: extends KinematicBody


onready var raycast = $RayCast;onready var eyes = $Eyes
onready var anim_player = $AnimationPlayer; var target

export var sweep = 0;var sweep_range = 90; var ray_length = -5
var health = 100;var speed = 5;var turning_speed = 10
var direction = Vector3();var fall = Vector3()
var gravity = 9.8

func _ready():
	yield(owner,"ready")
	target = owner.point

func _process(delta):
	if health < 50:queue_free()
	patrolling()
	if not is_on_floor():fall.y -= gravity * delta #Code for gravity.

func move_foward(speed = 7):
	direction -= transform.basis.z;direction = direction.normalized()
	move_and_slide(direction * speed, Vector3.UP,true)

func sweeping():
	sweep = wrapi(sweep + 30,-sweep_range,sweep_range)
	raycast.rotation_degrees.y = sweep

func patrolling():
	move_foward(speed)
	if not raycast.is_colliding():sweeping()
	else:avoid_obstacle()
	move_and_slide(fall, Vector3.UP)

func avoid_obstacle():
	if raycast.rotation_degrees.y < 0:rotate_y(deg2rad(turning_speed))
	elif raycast.rotation_degrees.y >= 0:rotate_y(deg2rad(-turning_speed))

Now here is what it looks like in action:

Now the biggest problem with my approach is that I haven't figured out how to make my "AI" chase down a target while avoiding obstacles. If I don't find a way to make the enemy avoid obstacles while chasing down a target, I might have to scrap this idea. I've just started brainstorming on how to solve this problem but, I also want to ask what do you think. Do you know of a simpler solution on how to get the enemies to avoid obstacles while chasing down a target?

@fire7side

Hmm...I think I've found it. Check this function out:

func chasing():
	var distance = transform.origin.distance_to(target.transform.origin)
	if target != null and distance > 1:
		move_foward(speed)
	if not raycast.is_colliding():
		if target != null:look_at(Vector3(translation.x,target.translation.y,translation.z),Vector3.UP)
		sweeping()
	else:avoid_obstacle()

Edit: I was wrong. :/

Edit again: Okay, so I made a small adjustment to my avoid_obstacle() script. I hope it will help. It goes like this:

func avoid_obstacle():
	if raycast.rotation_degrees.y < 0:
		rotate_y(deg2rad(turning_speed))
		direction -= transform.basis.z
	elif raycast.rotation_degrees.y >= 0:
		rotate_y(deg2rad(-turning_speed))
		direction += transform.basis.z

Let me know if you get it working so it moves to a target and avoids enemies. Yeah, 8 raycasts isn't bad, but if you have a lot of ai it would add up. There are probably a lot of ways of solving it, but I don't know how just one would be enough unless it was doing a sweep and that's basically the same thing. I'm kind of working on a game where I can actually try it out in a little bit. I wonder why it's not used much? I know the pathfinding from Godot is established, but it requires baking and probably wouldn't work in an open world. Although it is much more accurate for things like maze situations. Anyway, it was interesting looking at that solution and I learned some new things.

@fire7side

I think I'm dangerous close to finding a solution to the problem but, it involves having some sort of variable that sorts information about where the player( or target) is relative to the target. After that, I can have that information constantly updating other variables that need to be updated. I'll be back to show you what I have working so far and why it doesn't work all the time; I'll even back more video evidence. I think that both of us are on to something great but, I think you might have the solution. I suspect that the solution involves the Dot Product function but, I don't fully understand it or what it does, so I can't say for certain. See you tomorrow.

@fire7side

Hi I'm back. Sorry for taking so long to return; I was fighting my internet connection the entire day and it almost won. Here is a video demonstrating my AI chasing a target while avoiding obstacles:

Now the AI is far from perfect and I wish I could even show some footage of it failing. However, I'm tired now. Instead, I'll settle for telling you the limitations of this AI tomorrow. I'll also ask about the limitations of your AI as well, to see to what extend the limitations overlap and how they can be solved.

One thing that occurred to me is I might have an oblong object, like a car, which probably won't work, or will need modification. To tell the truth though, I decided to just use a plain path follow in my airplane game because I decided to do a reverse tower defense game, where the monsters are sort of towers and the airplane has to take them out before they shoot all the cars with xray vision. But yeah, I didn't test it much beyond that little video. Have to wait for another project. I still may use it on the monsters. Have them travel a little bit.

7 months later