Table of Contents

Raycasting

This C# Intermediate tutorial covers raycasting.

Explanation

Raycasting is an essential subject in 3D games. With raycasts we can detect if and what kinds of objects are in our line of sight. This can be used for detecting enemies or how far an object really is.

Raycast

This script sends out a raycast from the weapons barrel and sends it to an endpoint a little further. We check if we hit something along the way. If we do, we calculate the distance between the weapon barrel and the hit point. We then scale a laser to that distance to visualize the actual raycast. Depending on the collision group and filters, some objects are ignored.

// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using CSharpIntermediate.Code.Extensions;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Physics;

namespace CSharpIntermediate.Code
{
    public class RaycastDemo : SyncScript
    {
        public CollisionFilterGroupFlags CollideWithGroup;
        public bool CollideWithTriggers = false;
        public Entity HitPoint;

        private const float maxDistance = 4.0f;
        private Entity laser;
        private Simulation simulation;
      
        public override void Start()
        {
            //Store the physics simulation object
            simulation = this.GetSimulation();
            laser = Entity.FindChild("Laser");
        }

        public override void Update()
        {
            int drawX = 340;
            int drawY = 80;
            DebugText.Print("Press Q and E to raise/lower weapons", new Int2(drawX, drawY));

            var raycastStart = Entity.Transform.Position;
            var raycastEnd = Entity.Transform.Position + new Vector3(0, 0, maxDistance);
          
            drawY += 40;

            // Send a raycast from the start to the endposition
            if (simulation.Raycast(raycastStart, raycastEnd, out HitResult hitResult, CollisionFilterGroups.DefaultFilter, CollideWithGroup, CollideWithTriggers))
            {
                // If we hit something, calculate the distance to the hitpoint and scale the laser to that distance
                HitPoint.Transform.Position = hitResult.Point;
                var distance = Vector3.Distance(hitResult.Point, raycastStart);
                laser.Transform.Scale.Z = distance;

                DebugText.Print("Hit a collider", new Int2(drawX, drawY));
                DebugText.Print($"Raycast hit distance: {distance}", new Int2(drawX, drawY + 20));
                DebugText.Print($"Raycast hit point: {hitResult.Point.Print()}", new Int2(drawX, drawY + 40));
                DebugText.Print($"Raycast hit entity: {hitResult.Collider.Entity.Name}", new Int2(drawX, drawY + 60));
            }
            else
            {
                // If we didn't hit anything, scale the laser to match the distance between start and end
                HitPoint.Transform.Position = raycastEnd;
                laser.Transform.Scale.Z = Vector3.Distance(raycastStart, raycastEnd);
                DebugText.Print("No collider hit", new Int2(drawX, drawY));
            }
        }
    }
}

Penetrative raycast

In our first script, the raycast returns to us as soon as it hits the first object along its path. We can also send out a raycast to an endpoint, and let it return to us when it has reached its endpoint. It gives us back a list of objects that it has hit along the way. This list can be empty but also exists out of various objects. Depending on the collision group and filters, some objects are ignored.

// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using System.Collections.Generic;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Physics;

namespace CSharpIntermediate.Code
{
    public class RaycastPenetratingDemo : SyncScript
    {
        public CollisionFilterGroupFlags CollideWithGroup;
        public bool CollideWithTriggers = false;

        private Entity laser;
        private const float maxDistance = 3.0f;
        private Simulation simulation;

        public override void Start()
        {
            simulation = this.GetSimulation();
            laser = Entity.FindChild("Laser");
        }

        public override void Update()
        {
            int drawX = 700;
            int drawY = 80;
            DebugText.Print("Raycast penetration demo", new Int2(drawX, drawY));

            var raycastStart = Entity.Transform.Position;
            var raycastEnd = Entity.Transform.Position + new Vector3(0, 0, -maxDistance);

            var distance = Vector3.Distance(raycastStart, raycastEnd);
            laser.Transform.Scale.Z = distance;

            var hitResults = new List<HitResult>();
            simulation.RaycastPenetrating(raycastStart, raycastEnd, hitResults, CollisionFilterGroups.DefaultFilter, CollideWithGroup, CollideWithTriggers);

            drawY += 40;
            if (hitResults.Count > 0)
            {
                DebugText.Print($"Raycast has hit {hitResults.Count} object(s)", new Int2(drawX, drawY));

                foreach (var hitResult in hitResults)
                {
                    drawY += 20;
                    DebugText.Print($"- Raycast has hit: {hitResult.Collider.Entity.Name}", new Int2(drawX, drawY));
                }
            }
            else
            {
                DebugText.Print("No collider hit", new Int2(drawX, drawY));
            }
        }
    }
}