jquave 发表于 2021-11-30 08:37

用Unity实现《我们终将变成我们所关注的东西》

哈喽大家好我是Yumir,前几天偶然和朋友聊起一个很有趣的游戏,也许你在老早之前就听说并且玩过《信任的进化》。
这个游戏的作者Nicky Case是个大佬,大奖拿到手软,经历传奇,因为我刚刚看过他的演讲视频,所以我已经非常了解他啦ww。但我今天要说的是《We become what we behold》,是他的另一个游戏作品。
游戏地址:
坦白说我看到信任的进化的时候虽然觉得很厉害,但是并不觉得是个有趣的游戏,更像是大学老师的ppt【表打我】,但是他确实比直接去看书学博弈论更加有趣。
早先在大佬的推荐下拜读《游戏改变世界》,读后感慨如果我们的教育体系是一个及时反馈的游戏,那我们的学习生涯可太舒适了,但是显然这不是一件太容易的事情,《信任的进化》能够用一个简短的游戏流畅让玩家理解一个深奥的哲学理念,是我认为最惊艳的地方。
实际上这也是游戏一个比较不错的发展方向,甚至给我感觉很多大佬都是有这个意愿和期望的,只是最近好像都没听到相关消息,腾讯去年支持过几个,另一个比较火的就是《onehandclapping》吧,就是那个呦呦鹿鸣的游戏。
但是,相对作者其他的游戏我还是比较能get这个网页小游戏,寥寥几笔传达了不简单的思想,游戏流程不长却让你看到更多的东西。
游戏的内容是玩家扮演一名记者进行新闻拍摄,玩家拍摄到的图片会显示在一个小电视上,观众看了之后会有不同的反应,一件一件事情引起一系列的反应。
当我们在游戏结束之后大骂这个无良媒体时抬头看一眼游戏标题“我们终将变成我们所关注的东西”,恍然大悟,我们确实在一点点的让世界变成我们看到的样子,这个游戏才真正的结束了。
看到有趣的游戏当然要动手实现一下啦~特别这次还有热心原作者分享的素材包,可以不用自己画素材啦\^o^/!
----------------------------制作分割---------------------------------
一开始的计划是实现游戏中的拍照功能就好了,说是拍照实际上就是局部截图,试过很多方法没有头绪之后我决定先试着实现游戏的开始界面,毕竟局部截图的前提是全局截图,先实现开始界面也许就能懂得怎么实现拍照功能了。


这也是一个有趣的界面,如何实现小电视中有小电视中有小电视呢,可能你也想到了用小地图的方法,利用rawimage将整个屏幕投射到小电视上不就好了么?首先动手做一个小电视:

[*]新建一个RawImage和一个RenderTexture。
[*]新建一个用于拍摄的摄像机。
[*]将RenderTexture拖拽到对应的位置赋值。


但是摄像机并不会像我预想中的那样将我们在游戏中看到的画面渲染到小电视上,他渲染的是空无一物的场景,也就是说,UI在摄像机眼里并不是“他看到”的东西,为了让UI处于摄像机的视野范围我将Canvas设置为“WorldSpace”模式。


但是这样一来只能拍到没有RenderTexture的画面,通过实验判断是因为渲染摄像机画面时画面中的RawImage的贴图上没有任何东西,也就是我们需要多层渲染。


为了让小电视里面的小电视显示新的小电视,最简单直接的方法就是假嵌套——也就是复制多份RawImage排列出镜中镜的假象,毕竟当图足够小的时候,图里的小电视有没有画面就看不出来了,但如果我鼠标运动到小电视上呢?
另一个办法则是我从假嵌套的方法中提炼思考得到的,通过查阅资料得知unity可以利用Texture2D提取指定的图像像素,那么要如何使图像不断地渲染叠加呢?我是这样实现的:
   IEnumerator Show(Rect rect)
    {
      RenderTexture rt = new RenderTexture((int)rect.width, (int)rect.height, 0);
      theCamera.targetTexture = rt;
      Texture2D screenShot = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGB24, false);
      while (true)
      {
            yield return new WaitForEndOfFrame();
            //theCamera.Render();
            RenderTexture.active = rt;
            screenShot.ReadPixels(new Rect(0,0, Screen.width, Screen.height), 0, 0);
            screenShot.Apply();

            rawImage1.texture = screenShot;
            
            //theCamera.targetTexture = null;
            RenderTexture.active = null;
      }
    }
在Start方法中启用这个协程,该协程会在每次摄像机渲染完成之后提取相机的RenderTexture中的像素,并复制给小电视的Texture,这样一来在下一次渲染的时候小电视上面就是有画面的了,叠加之后就形成我们需要的效果。


由于UI的模式是WorldSpace,所以只能用射线检测的方法来确定鼠标图标的位置,这样一来对另一个场景中的功能实现思路就清晰了很多。
我是这样拆解我的实现过程(思路)的:

[*]首先我需要实现可以拍到局部图像的相机,并且这个相机不会拍到用来表示相机的黑框。
[*]为了得到照片中拍到的游戏物体信息,我需要将相关物体都加上碰撞器,并且使相机黑框在场景中有所属的碰撞体。
[*]以上所有的功能实现后再实现动画效果以及内容判断逻辑的测试。
在开始制作之前需要搭建场景,除了需要用到的RawImage之外其他的物体都用Sprite2D图片精灵,并且设置相应的Tag,由于相机只会根据“Canvas”的“Layer”来过滤渲染对象,所以需要两个Canvas,分别设置Layer。
场景中依旧需要两个相机,将用于拍摄的相机的Size调整为合适大小,CulingMask设置为屏蔽相机黑框,使摄像机既可以拍摄到和我们设置的相机黑框一样大小的图像,又不会将相机黑框渲染出来。


相机的移动则是通过射线检测实现的。
raycast = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero);
if (raycast.transform != null)
{
    transform.position = raycast.point;
    camCollider.transform.position = raycast.point;
    myCamera.transform.position = new Vector3(raycast.point.x, raycast.point.y, myCamera.transform.position.z);
}新建一个RenderTexture拖拽到用于摄像的相机上,并且在脚本中持有他,当鼠标左键按下时将摄像机的RenderTexture读取到对应的Raw Image上,于是我们就可以想拍多少层小电视就拍多少层小电视啦。


之所以要另外新建一个RenderTexture是因为在代码中控制生成的RenderTexture比较不方便,通过面板可以更直观的控制,并且方便像素读取。


接下来制作一个和黑框等大的碰撞体,将游戏物体的组件设置如下:


脚本上声明了一个list数组,用于存储在碰撞体中的游戏物体的引用,这样一来通过获取该脚本上的这个list数组就可以进行新闻类型的判断,通过不同的Tag返回的数值来控制不同的反馈。这里我只写了两个最简单的情况,并不打算实现复杂的逻辑,你可以试着去分析一下。
   private int NewType(List<GameObject> something)
    {
      int flag = 0;
      if (something.Count == 0)
      {
            return 0;
      }
      foreach (GameObject item in something)
      {
            if (item.tag == "TV")
            {
                flag = 1;
            }
      }
      if (flag != 0)
      {
            return flag;
      }
      //筛选特殊人物返回特定值,剩下就是群众演员
      List<GameObject> gg = new List<GameObject>();
      foreach (GameObject item in something)
      {

      }

      return -1;
    }最后就是拍摄的动画效果实现了,因为摄像机移动的目标点不固定所以无法直接用自带的动画系统解决,这里添加了一个Dotween插件,在官方商店可以免费下载。


在脚本开头引用该命名空间(DG.Tweening)就可以开始手写动画了,别怕,其实一点也不复杂。
由于在动画期间玩家不能再控制相机移动所以我们需要一个布尔值来限制操作,当鼠标按下时读取当前摄制的图像,并且开始拍摄协程,在协程的开始关闭鼠标控制,结束时再解开。
首先将黑框上携带的RawImage显示出来,并且开始一段主摄像机移向摄像摄像机并缩小视口的动画。
rawImage.gameObject.SetActive(true);
rawImage.texture = texture;
mainCamera.DOOrthoSize(myCamera.orthographicSize, 2.0f);
mainCamera.transform.DOMove(myCamera.transform.position, 2.0f);上一段动画播放结束后,将主摄像机的位置切换到小电视的位置,这我手动测量了一下在这个位置的摄像机Size,直接赋值。
TVRawImage.texture = texture;
rawImage.gameObject.SetActive(false);
mainCamera.transform.position = new Vector3(TVRawImage.transform.position.x, TVRawImage.transform.position.y, mainCamera.transform.position.z);
mainCamera.orthographicSize = 0.7f;同时需要根据图像内容显示新闻标题,标题部分的动画我是通过自带的动画录制的,录制方法在我的第一篇文章有提到过:
停顿一段时间之后开始缩小视图,这里需要在脚本中记录摄像机的初始位置信息,再分两个阶段动画恢复即可。
//缩小视图
mainCamera.DOOrthoSize(mainCamOrthographicSize / 2, 1.0f);

yield return new WaitForSeconds(1.5f);

mainCamera.DOOrthoSize(mainCamOrthographicSize, 1.0f);
mainCamera.transform.DOMove(mainCamPoint, 1.0f);这样一来就大功告成啦,不知道会不会像seed一样有同学发散出更多的内容,如果有的话可以和大家分享一下哦。


知乎上可以分析的内容始终有限,详细内容可以下载我的Github项目进行了解。
写下这篇文章的时候一直在感慨,内容看着简单,背后其实经历了无数次失败,在多次尝试简化后才是现在简单的几行代码,在制作的过程中几乎是一步一个坑,占着地利疯狂骚扰皮皮关的各位老师答疑解惑,最终终于将所有功能实现出来。
在此感谢皮皮关的诸位老师,和无私分享源码素材的原作者。
Nicky Case的其他游戏:
——分割线——
欢迎加入游戏开发群欢乐搅基:610475807
对游戏开发感兴趣的童鞋可戳这里进一步了解:http://www.levelpp.com/

NoiseFloor 发表于 2021-11-30 08:43

起飞

LiteralliJeff 发表于 2021-11-30 08:44

请问您是否了解Nicky Case制作的第一个Flash小游戏:the game:及其内容?虽然这事情已经过去多年,不过我还是希望您能对这个人辩证地看待,他固然有才,但14岁的时候做出那种东西,还是蛮缺德的。(又及:不懂Unity,好厉害的样子)

DomDomm 发表于 2021-11-30 08:49

不清楚。单纯是看到这个游戏有趣进行复刻。

LiteralliJeff 发表于 2021-11-30 08:53

收到。很佩服您这种会写程序又爱钻研的人,这个评论只是针对Nicky Case的。那个游戏的内容涉政,我不爽的点主要是毛、藏、奥运这几个。现在想来,一个14岁的少年,自幼生活在反g宣传中,把自己过剩的才华用到zz讽刺上也不让人意外。我去年看过他写的那篇关于新冠的文章,看得出他现在理性多了。只能说提高我国国际形象任重道远,振兴中华我辈义不容辞……
页: [1]
查看完整版本: 用Unity实现《我们终将变成我们所关注的东西》