镜面反射谜题

  • 需求
  • 思路
  • 遇见的坑
  • 成果


需求

最近策划提出了一个新的解密需求,有三束不同颜色的激光,还有几面角度不一的镜子,以及三个一一对应颜色的宝石。玩家需要通过移动镜子的位置来反射激光,让激光照射到和他相同颜色的镜子。
听起来好像很简单,但是还有两个关键的条件:

  1. 激光会互相阻碍,相互阻碍的激光会停止到交汇地点。
  2. 如果两束激光在镜子处交汇,则按照红、绿、蓝的优先级进行阻挡。也就是说,红色的优先级会比绿色和蓝色的优先级更高,就代表了是这个颜色的激光优先打到镜子。

思路

对于镜子位置和旋转固定的谜题来说其实最好的解决方案就是按照格子去计算激光应该在哪个位置,一格一格的计算,这样会比较省事。
比如说红色光线走3格然后遇见右转的镜子,开始向右走,比如又走了4格,这个同时绿色的激光也应该走了7格,然后发现走的第七格红色的已经走过了,那就代表这时候两个激光是互相阻挡了,红色的也就没有必要往前再走了。两个激光就交汇到绿色第七格(红色第六格)。至于三个激光互相阻挡的情况不予考虑,在游戏设计的时候会尽量避免这个情况的发生。
虽然我们游戏确实是比较适合上面说的解决方案,但是我却用了下面的方法去写的:
每个激光发射器就正常的打射线,遇见镜子就反射,遇见墙体就停止。将所有的反射点记存到集合中,拿出每一段的线段和其他两个线段进行相交验证,如果验证相交后将相交点记录下来。然后根据优先级原则筛选到底使用哪个交点作为最后的终点。

射线反射的代码:

Vector3 inDirection = Vector3.Reflect(hit.point - ray.origin, hit.normal);
ray = new Ray(hit.point, inDirection);

计算线段是否相交:

/// <summary>
        /// 计算AB与CD两条线段的交点.
        /// </summary>
        /// <param name="a">A点</param>
        /// <param name="b">B点</param>
        /// <param name="c">C点</param>
        /// <param name="d">D点</param>
        /// <param name="intersectPos">AB与CD的交点</param>
        /// <returns>是否相交 true:相交 false:未相交</returns>
        private bool TryGetIntersectPoint(Vector3 a, Vector3 b, Vector3 c, Vector3 d, out Vector3 intersectPos)
        {
            intersectPos = Vector3.zero;

            Vector3 ab = b - a;
            Vector3 ca = a - c;
            Vector3 cd = d - c;

            Vector3 v1 = Vector3.Cross(ca, cd);

            if (a==c||a==d)
            {
                intersectPos = a;
                return true;
            }
            if (b==c||b==d)
            {
                intersectPos = b;
                return true;
            }
            //if (Mathf.Abs(Vector3.Dot(v1, ab)) > 1e-6)
            //{
            //    // 不共面
            //    return false;
            //}

            if (Vector3.Cross(ab, cd).sqrMagnitude <= 1e-6)
            {
                // 平行
                return false;
            }

            Vector3 ad = d - a;
            Vector3 cb = b - c;
            // 快速排斥
            if (Mathf.Min(a.x, b.x) > Mathf.Max(c.x, d.x) || Mathf.Max(a.x, b.x) < Mathf.Min(c.x, d.x)
               //|| Mathf.Min(a.y, b.y) > Mathf.Max(c.y, d.y) || Mathf.Max(a.y, b.y) < Mathf.Min(c.y, d.y)
               || Mathf.Min(a.z, b.z) > Mathf.Max(c.z, d.z) || Mathf.Max(a.z, b.z) < Mathf.Min(c.z, d.z)
            )
                return false;

            // 跨立试验
            if (Vector3.Dot(Vector3.Cross(-ca, ab), Vector3.Cross(ab, ad)) > 0
                && Vector3.Dot(Vector3.Cross(ca, cd), Vector3.Cross(cd, cb)) > 0)
            {
                Vector3 v2 = Vector3.Cross(cd, ab);
                float ratio = Vector3.Dot(v1, v2) / v2.sqrMagnitude;
                intersectPos = a + ab * ratio;
                return true;
            }

            return false;


        }

遇见的坑

我代码按照思路都写好了,一测试,嗯?相交不上。怎么回事?打断点后发现,你以为的两个线段相交也仅仅是你以为,因为镜子的位置、发射器的位置等等原因,导致你看到应该在某一点相交的两条线却在该点其实就差了零点几,所以一定要注意射线的高度、镜子的旋转和位置。我就直接相似处理了。如果两个点的距离相差不过0.01我就让他们归为一个点。这样在运行果然就成功了。
具体代码就不放出来了。

成果


激光谜题