Rendering/GrappleLineRenderer.cs

A rendering helper component for the BlockParty game. It rasterizes a grapple rope as a 1-pixel-wide Bresenham line on the logical game-pixel grid, emits each pixel as two triangles into a vertex list, and draws that list in a SceneCustomObject render override on the depth-carrying overlay layer at a depth converted from an integer layer index.

using System.Collections.Generic;
using Sandbox;

namespace BlockParty;

/// <summary>Draws a one-pixel Bresenham line directly on the logical game-pixel grid.</summary>
public sealed class GrappleLineRenderer : Component
{
	static readonly Material Material = Material.FromShader( "shaders/grapple_line.shader" );
	const float ENDPOINT_INSET = 0.001f;

	readonly List<Vertex> _vertices = new( Arena.WIDTH * 6 );
	SceneCustomObject _sceneObject;

	protected override void OnEnabled()
	{
		_sceneObject = new SceneCustomObject( Scene.SceneWorld )
		{
			Transform = global::Transform.Zero,
			RenderLayer = SceneRenderLayer.OverlayWithDepth,
			RenderOverride = OnRender,
		};
		_sceneObject.Flags.IsOpaque = false;
		_sceneObject.Flags.IsTranslucent = true;
		_sceneObject.Flags.CastShadows = false;
	}

	protected override void OnDisabled()
	{
		_sceneObject?.Delete();
		_sceneObject = null;
	}

	public void SetLine( Vector2 start, Vector2 end, Color color, int depth )
	{
		_vertices.Clear();
		if ( _sceneObject is null ) return;

		Vector2 delta = end - start;
		float length = delta.Length;
		if ( length <= ENDPOINT_INSET * 2f ) return;

		// Exact integer endpoints lie between two logical pixels. Select the cells inside the segment
		// so opposite directions neither overlap the player nor disappear beneath the hit surface.
		Vector2 inset = delta / length * ENDPOINT_INSET;
		Vector2 rasterStart = start + inset;
		Vector2 rasterEnd = end - inset;
		int x = (int)MathF.Floor( rasterStart.x );
		int y = (int)MathF.Floor( rasterStart.y );
		int endX = (int)MathF.Floor( rasterEnd.x );
		int endY = (int)MathF.Floor( rasterEnd.y );
		int deltaX = Math.Abs( endX - x );
		int stepX = x < endX ? 1 : -1;
		int deltaY = -Math.Abs( endY - y );
		int stepY = y < endY ? 1 : -1;
		int error = deltaX + deltaY;
		float z = Globals.DepthToZ( depth );

		while ( true )
		{
			AddPixel( x, y, z, color );
			if ( x == endX && y == endY ) break;

			int doubledError = error * 2;
			if ( doubledError >= deltaY )
			{
				error += deltaY;
				x += stepX;
			}
			if ( doubledError <= deltaX )
			{
				error += deltaX;
				y += stepY;
			}
		}
	}

	public void Clear() => _vertices.Clear();

	void AddPixel( int x, int y, float z, Color color )
	{
		Vertex V( float px, float py ) => new( new Vector3( px, py, z ), color );
		_vertices.Add( V( x, y ) );
		_vertices.Add( V( x + 1f, y ) );
		_vertices.Add( V( x + 1f, y + 1f ) );
		_vertices.Add( V( x, y ) );
		_vertices.Add( V( x + 1f, y + 1f ) );
		_vertices.Add( V( x, y + 1f ) );
	}

	void OnRender( SceneObject sceneObject )
	{
		if ( _vertices.Count == 0 || Material is null ) return;
		Graphics.Draw( _vertices, _vertices.Count, Material, primitiveType: Graphics.PrimitiveType.Triangles );
	}
}