using System; using System.Collections.Generic; using Godot; /// <summary> /// 液体画布 /// </summary> public partial class LiquidCanvas : Sprite2D, IDestroy { /// <summary> /// 程序每帧最多等待执行时间, 超过这个时间的像素点将交到下一帧执行, 单位: 毫秒 /// </summary> public static float MaxWaitTime { get; set; } = 4f; /// <summary> /// 画布缩放 /// </summary> public static int CanvasScale { get; } = 4; public bool IsDestroyed { get; private set; } private Image _image; private ImageTexture _texture; //画布上的像素点 private LiquidPixel[,] _imagePixels; //需要执行更新的像素点 private List<LiquidPixel> _updateImagePixels = new List<LiquidPixel>(); //画布已经运行的时间 private float _runTime = 0; private int _executeIndex = -1; //用于记录补间操作下有变动的像素点 private List<LiquidPixel> _tempList = new List<LiquidPixel>(); //记录是否有像素点发生变动 private bool _changeFlag = false; //所属房间 private RoomInfo _roomInfo; public LiquidCanvas(RoomInfo roomInfo, int width, int height) { _roomInfo = roomInfo; Centered = false; Material = ResourceManager.Load<Material>(ResourcePath.resource_material_Sawtooth_tres); _image = Image.Create(width, height, false, Image.Format.Rgba8); _texture = ImageTexture.CreateFromImage(_image); Texture = _texture; _imagePixels = new LiquidPixel[width, height]; } public void Destroy() { if (IsDestroyed) { return; } IsDestroyed = true; QueueFree(); _texture.Dispose(); _image.Dispose(); } public override void _Process(double delta) { //这里待优化, 应该每次绘制都将像素点放入 _tempList 中, 然后帧结束再统一提交 //更新消除逻辑 if (_updateImagePixels.Count > 0) { var startIndex = _executeIndex; if (_executeIndex < 0 || _executeIndex >= _updateImagePixels.Count) { _executeIndex = _updateImagePixels.Count - 1; } var startTime = DateTime.UtcNow; var isOver = false; var index = 0; for (; _executeIndex >= 0; _executeIndex--) { index++; var imagePixel = _updateImagePixels[_executeIndex]; if (UpdateImagePixel(imagePixel)) //移除 { _updateImagePixels.RemoveAt(_executeIndex); if (_executeIndex < startIndex) { startIndex--; } } if (index > 200) { index = 0; if ((DateTime.UtcNow - startTime).TotalMilliseconds > MaxWaitTime) //超过最大执行时间 { isOver = true; break; } } } if (!isOver && startIndex >= 0 && _executeIndex < 0) { _executeIndex = _updateImagePixels.Count - 1; for (; _executeIndex >= startIndex; _executeIndex--) { index++; var imagePixel = _updateImagePixels[_executeIndex]; if (UpdateImagePixel(imagePixel)) //移除 { _updateImagePixels.RemoveAt(_executeIndex); } if (index > 200) { index = 0; if ((DateTime.UtcNow - startTime).TotalMilliseconds > MaxWaitTime) //超过最大执行时间 { break; } } } } } if (_changeFlag) { _texture.Update(_image); _changeFlag = false; } _runTime += (float)delta; } /// <summary> /// 将画布外的坐标转为画布内的坐标 /// </summary> public Vector2I ToLiquidCanvasPosition(Vector2 position) { return (_roomInfo.ToCanvasPosition(position) / CanvasScale).AsVector2I(); } /// <summary> /// 根据画笔数据在画布上绘制液体, 转换坐标请调用 ToLiquidCanvasPosition() 函数 /// </summary> /// <param name="brush">画笔数据</param> /// <param name="position">绘制坐标, 相对于画布坐标</param> public void DrawBrush(BrushImageData brush, Vector2I position) { DrawBrush(brush, null, position, 0); } /// <summary> /// 根据画笔数据在画布上绘制液体, 转换坐标请调用 ToLiquidCanvasPosition() 函数 /// </summary> /// <param name="brush">画笔数据</param> /// <param name="position">绘制坐标, 相对于画布坐标</param> /// <param name="rotation">旋转角度, 弧度制</param> public void DrawBrush(BrushImageData brush, Vector2I position, float rotation) { DrawBrush(brush, null, position, rotation); } /// <summary> /// 根据画笔数据在画布上绘制液体, 转换坐标请调用 ToLiquidCanvasPosition() 函数 /// </summary> /// <param name="brush">画笔数据</param> /// <param name="prevPosition">上一帧坐标, 相对于画布坐标, 改参数用于两点距离较大时执行补间操作, 如果传 null, 则不会进行补间</param> /// <param name="position">绘制坐标, 相对于画布坐标</param> /// <param name="rotation">旋转角度, 弧度制</param> public void DrawBrush(BrushImageData brush, Vector2I? prevPosition, Vector2I position, float rotation) { var center = new Vector2I(brush.Width, brush.Height) / 2; var pos = position - center; var canvasWidth = _texture.GetWidth(); var canvasHeight = _texture.GetHeight(); //存在上一次记录的点 if (prevPosition != null) { var offset = new Vector2(position.X - prevPosition.Value.X, position.Y - prevPosition.Value.Y); var maxL = brush.Material.Ffm * Mathf.Lerp( brush.PixelHeight, brush.PixelWidth, Mathf.Abs(Mathf.Sin(offset.Angle() - rotation + Mathf.Pi * 0.5f)) ); var len = offset.Length(); if (len > maxL) //距离太大了, 需要补间 { //Debug.Log($"距离太大了, 启用补间: len: {len}, maxL: {maxL}"); var count = Mathf.CeilToInt(len / maxL); var step = new Vector2(offset.X / count, offset.Y / count); var prevPos = prevPosition.Value - center; for (var i = 1; i <= count; i++) { foreach (var brushPixel in brush.Pixels) { var brushPos = RotatePixels(brushPixel.X, brushPixel.Y, center.X, center.Y, rotation); var x = (int)(prevPos.X + step.X * i + brushPos.X); var y = (int)(prevPos.Y + step.Y * i + brushPos.Y); if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) { var temp = SetPixelData(x, y, brushPixel); if (!temp.TempFlag) { temp.TempFlag = true; _tempList.Add(temp); } } } } foreach (var brushPixel in brush.Pixels) { var brushPos = RotatePixels(brushPixel.X, brushPixel.Y, center.X, center.Y, rotation); var x = pos.X + brushPos.X; var y = pos.Y + brushPos.Y; if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) { var temp = SetPixelData(x, y, brushPixel); if (!temp.TempFlag) { temp.TempFlag = true; _tempList.Add(temp); } } } foreach (var imagePixel in _tempList) { _changeFlag = true; _image.SetPixel(imagePixel.X, imagePixel.Y, imagePixel.Color); imagePixel.TempFlag = false; } _tempList.Clear(); return; } } foreach (var brushPixel in brush.Pixels) { var brushPos = RotatePixels(brushPixel.X, brushPixel.Y, center.X, center.Y, rotation); var x = pos.X + brushPos.X; var y = pos.Y + brushPos.Y; if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) { _changeFlag = true; var temp = SetPixelData(x, y, brushPixel); _image.SetPixel(x, y, temp.Color); } } } /// <summary> /// 返回是否碰到任何有效像素点 /// </summary> public bool Collision(int x, int y) { if (x >= 0 && x < _imagePixels.GetLength(0) && y >= 0 && y < _imagePixels.GetLength(1)) { var result = _imagePixels[x, y]; if (result != null && result.IsRun) { return true; } } return false; } /// <summary> /// 返回碰撞到的有效像素点数据 /// </summary> public LiquidPixel GetPixelData(int x, int y) { if (x >= 0 && x < _imagePixels.GetLength(0) && y >= 0 && y < _imagePixels.GetLength(1)) { var result = _imagePixels[x, y]; if (result != null && result.IsRun) { return result; } } return null; } /// <summary> /// 更新像素点数据逻辑, 返回是否擦除 /// </summary> private bool UpdateImagePixel(LiquidPixel imagePixel) { if (imagePixel.Color.A > 0) { if (imagePixel.Timer > 0) //继续等待消散 { imagePixel.Timer -= _runTime - imagePixel.TempTime; imagePixel.TempTime = _runTime; } else { imagePixel.Color.A -= imagePixel.Material.WriteOffSpeed * (_runTime - imagePixel.TempTime); if (imagePixel.Color.A <= 0) //完全透明了 { _changeFlag = true; _image.SetPixel(imagePixel.X, imagePixel.Y, new Color(0, 0, 0, 0)); imagePixel.IsRun = false; imagePixel.IsUpdate = false; return true; } else { _changeFlag = true; _image.SetPixel(imagePixel.X, imagePixel.Y, imagePixel.Color); imagePixel.TempTime = _runTime; } } } return false; } //记录像素点数据 private LiquidPixel SetPixelData(int x, int y, BrushPixelData pixelData) { var temp = _imagePixels[x, y]; if (temp == null) { temp = new LiquidPixel() { X = x, Y = y, Color = pixelData.Color, Material = pixelData.Material, Timer = pixelData.Material.Duration, }; _imagePixels[x, y] = temp; temp.IsRun = true; temp.IsUpdate = temp.Material.Duration >= 0; if (temp.IsUpdate) { _updateImagePixels.Add(temp); } temp.TempTime = _runTime; } else { if (temp.Material != pixelData.Material) { temp.Color = pixelData.Color; temp.Material = pixelData.Material; } else { var tempColor = pixelData.Color; temp.Color = new Color(tempColor.R, tempColor.G, tempColor.B, Mathf.Max(temp.Color.A, tempColor.A)); } temp.Timer = pixelData.Material.Duration; var prevUpdate = temp.IsUpdate; temp.IsUpdate = temp.Material.Duration >= 0; if (!prevUpdate && temp.IsUpdate) { _updateImagePixels.Add(temp); } else if (prevUpdate && !temp.IsUpdate) { _updateImagePixels.Remove(temp); } temp.IsRun = true; temp.TempTime = _runTime; } return temp; } /// <summary> /// 根据 rotation 旋转像素点坐标, 并返回旋转后的坐标, rotation 为弧度制角度, 旋转中心点为 centerX, centerY /// </summary> private Vector2I RotatePixels(int x, int y, int centerX, int centerY, float rotation) { if (rotation == 0) { return new Vector2I(x, y); } x -= centerX; y -= centerY; var sv = Mathf.Sin(rotation); var cv = Mathf.Cos(rotation); var newX = Mathf.RoundToInt(x * cv - y * sv); var newY = Mathf.RoundToInt(x * sv + y * cv); newX += centerX; newY += centerY; return new Vector2I(newX, newY); } }