本帖最后由 贰逼 于 2017-1-23 13:34 编辑

第三人称游戏都有一个由引擎控制的第三人称控制器,那么这在Minecraft中能否实现?
只能说达到一半。

        第三人称控制器(Third Person Controller)是角色控制器的一种,较比第一人称控制器可以反映更多信息包括角色的建模/动画/行为,以及更加多样的操作模式。第三人称控制器最近的一个例子就是Minecraft中的F5视角,即第三人称视角。
        当然比起像Minecraft这种可以直接依赖实时演算的第三人称控制器,这篇帖子里所讲的是对于目前Minecraft原版来说依靠命令方块力所能及的控制器。具体来讲,就是没有进行三维旋转的功能。原因很简单,一是无法使用相关函数,二是穷举即使精度为5°的三维旋转也特别消耗资源。
        总体来说,Minecraft原版最大限度可以还原一款2.5D游戏
        所以这篇帖子讨论的第三人称控制器是基于玩家视角不动,但是能通过自身操作操作非关联自身的实体角色(什么叫关联自身其实很明白,就是利用execute指令对玩家的原始坐标相对操作。例如你可以使用/execute @p ~ ~ ~ tp @e[tag=Character] ~ ~-5 ~制作一个最简单的关联自身的控制器)
一个gif展示:(15.7M,耐心加载)





Demo截图:
基本实现了角色移动,角色切换。

点击按钮进入游戏

F1关闭游戏GUI以获得最佳体验

WASD移动

绿柱用于切换角色


物品栏中单击下图的Exit退出游戏




按键操作事件的监听
  • 大前提
由于我们在此讨论的控制器固定视角,所以接下来一切方案都是建立在玩家坐标、视角都固定的情况下

在正式开始讲之前,我们先要来了解一下原理。
而在了解原理之前,我们需要了解一个概念:事件监听
拿一个按钮作为例子,比如你的登录器,你点击"Play"按钮,这个按钮就会发生这个叫做OnClick的事件。事件监听就是内部检测这个事件进行事件监听并监听到该事件的发生即可关联或调用相关操作
你的鼠标按左键也有三种事件(OnMouseDown()鼠标刚按下、OnMouse()鼠标按下的状态、OnMouseUp()鼠标刚松开),这些事件都可以被内部监听。

而要在Minecraft中做到各种事件监听,要结合各种各样的方法:
  • WASD移动事件
检测玩家移动,其实是非常难做到的,因为迄今为止没有一个游戏中的机制特别关联玩家移动,并且可以被指令方块检测,除了玩家坐标的变化。而检测玩家的坐标,我们自然而然想到了Detect。只是具体靠什么精确地检测,我们可以给出下面这种检测方案:

假设玩家在其立足点坐标中心,也就是(x.500,y.000,z.500),面向z+方向

检测按下'W'(向前/x+方向),就是检测玩家对于前方的蓝色方块距离是否缩小,也就是要相对玩家向前检测前方距离<0.5是否为蓝色方块
  1. /execute @a[tag=Controlling] ~ ~ ~ detect ~ ~ ~0.49999999999995 wool 11
复制代码
同理,按下'A''S''D'就是相对玩家向各方向检测距离<0.5是否为相应方块
  1. /execute @a[tag=Controlling] ~ ~ ~ detect ~ ~ ~-0.49999999999995 wool 4
复制代码
  1. /execute @a[tag=Controlling] ~ ~ ~ detect ~0.49999999999995 ~ ~ wool 5
复制代码
  1. /execute @a[tag=Controlling] ~ ~ ~ detect ~-0.49999999999995 ~ ~ wool 1
复制代码
至于为什么用0.49999999999995,这是因为float的限制,就相当于0.49999999999995无限接近0.5,只要玩家向前移动了0.00000000000005格就能被检测到。

除了上述逐个检测的方法外,我们可以更加简化地,检测玩家向各方向的顶部是否为空气

(四个彩块都代表空气)拿前进为例,由于头顶上是一个存在的方块,玩家没有前进时检测前方空气距离<0.5为空气不成立,只有玩家向前移动后与前方空气的距离才会<0.5。以此类推,各方向都可以直接靠检测空气实现监听。
对应的指令为:
  1. execute @a[tag=Controlling] ~ ~ ~ detect ~ ~2 ~0.49999999999995 air 0
  2. execute @a[tag=Controlling] ~ ~ ~ detect ~ ~2 ~-0.49999999999995 air 0
  3. execute @a[tag=Controlling] ~ ~ ~ detect ~0.49999999999995 ~2 ~ air 0
  4. execute @a[tag=Controlling] ~ ~ ~ detect ~-0.49999999999995 ~2 ~ air 0
复制代码

有关detect之后的操作将会在后面"移动操作"中进一步讲解

  • Space空格事件
有了之前的移动的基础,由于Space键作为Minecraft的跳跃键,本质监听方式是一样的,最后的指令即:
  1. execute @a[score_control_min=1] ~ ~ ~ detect ~ ~2.99999 ~ air 0
复制代码
也就是当玩家刚刚起跳检测黑块的位置是空气来实现监听

为什么分开来讲?因为在第三人称控制中,Space不止作为移动的跳跃键,也可以是一个功能键,具体detect后的操作在后面"其他操作"中进一步讲解

  • Shift事件
Shift也是一个热门操作按键,具体方法:
点击这篇帖子查看原理讲解
同样检测后的操作在后面"其他操作"中进一步讲解



移动操作
  • 基本操作
监听之后最基本的操作就是添加移动的指令,如下:
  1. execute @a[tag=Controlling] ~ ~ ~ detect ~ ~2 ~0.49999999999995 air 0 execute @e[tag=character] ~ ~ ~ detect ~ ~1 ~0.9 air 0 tp @e[tag=character] ~ ~ ~0.6
  2. execute @a[tag=Controlling] ~ ~ ~ detect ~ ~2 ~0.49999999999995 air 0 execute @e[tag=character] ~ ~ ~ tp @e[tag=character] ~ ~ ~ 0 ~
  3. execute @a[tag=Controlling] ~ ~ ~ detect ~ ~2 ~-0.49999999999995 air 0 execute @e[tag=character] ~ ~ ~ detect ~ ~1 ~-0.9 air 0 tp @e[tag=character] ~ ~ ~-0.6
  4. execute @a[tag=Controlling] ~ ~ ~ detect ~ ~2 ~-0.49999999999995 air 0 execute @e[tag=character] ~ ~ ~ tp @e[tag=character] ~ ~ ~ 180 ~
  5. execute @a[tag=Controlling] ~ ~ ~ detect ~0.49999999999995 ~2 ~ air 0 execute @e[tag=character] ~ ~ ~ detect ~0.9 ~1 ~ air 0 tp @e[tag=character] ~0.6 ~ ~
  6. execute @a[tag=Controlling] ~ ~ ~ detect ~0.49999999999995 ~2 ~ air 0 execute @e[tag=character] ~ ~ ~ tp @e[tag=character] ~ ~ ~ -90 ~
  7. execute @a[tag=Controlling] ~ ~ ~ detect ~-0.49999999999995 ~2 ~ air 0 execute @e[tag=character] ~ ~ ~ detect ~-0.9 ~1 ~ air 0 tp @e[tag=character] ~-0.6 ~ ~
  8. execute @a[tag=Controlling] ~ ~ ~ detect ~-0.49999999999995 ~2 ~ air 0 execute @e[tag=character] ~ ~ ~ tp @e[tag=character] ~ ~ ~ 90 ~
复制代码
具体内容就是将角色移动后再改变角色所面朝的方向(tag=character表示操作的角色)

  • 重置玩家视角和坐标
前面提过的大前提,因此在玩家移动后我们还需要重置玩家的坐标和视角。
首先建立一个moved计分板/dummy,并将其用stats关联到实体
  1. /scoreboard players set @e[tag=character] moved 0
  2. stats entity @e[tag=character] set AffectedEntities @e[tag=character] moved
复制代码
这样只要对实体进行操作,moved计分板就会有分数。
接下来就是重置的工作了
  1. tp @a[tag=Controlling] ~ ~ ~ [dx] [dy]
  2. execute @e[tag=character,score_moved_min=1] ~ ~ ~ tp @a[tag=Controlling] [x] ~ [z]
  3. cond:scoreboard players set @e[tag=character] moved 0
复制代码
首先是固定玩家的视角到(dx,dy)(可自定义,即玩家视角望向的方向)。当检测到moved有分数,就传送玩家到(x,~,z)(可自定义,即玩家固定的坐标,也就是前面使用玩家头颅代表的玩家立足点中心坐标),然后重置moved。

  • 执行顺序
重置必须在移动操作之前,这是因为假如先进行移动操作,再重置,在经过该次执行后,所表现出来的效果仍旧是重置后的,因此达不到我们的预期效果。
并且移动操作监听其实并非并列进行,这是由于链式模块都有单向顺序,刷新周期0.05s,而Minecraft中实际的玩家移动是Update同步进行,模块执行和实际玩家移动存在时间差。总的来说如果使用了不恰当的玩家移动监听顺序,某些方向的移动监听也许会失去效果。
最后的恰当顺序请看"最终指令"



其他操作
  • Space操作
在监听完后你可以使用红石块激活指定模块:
  1. execute @a[tag=Controlling] ~ ~ ~ detect ~ ~2.99999 ~ air 0 execute @p ~ ~ ~ detect [x] [y] [z] air 0 setblock [x] [y] [z] redstone_block 0 replace
复制代码
检测(x,y,z)是否没有红石块再放置红石块来激活模块(这样做是为了防止一个需要延迟操作时间的模块在短时间内被反复调用。例如跳跃需要时间下落,如果在下落过程中继续跳跃,这不就等于无限续跳了吗)
作为一个Minecraft的移动键,同样也受执行顺序影响。最后的恰当顺序请看"最终指令"

  • Shift操作
  1. /tp @e[name=sneak] ~ ~-300 ~
  2. /kill @e[name=sneak]
  3. /summon minecraft:rabbit ~ ~ ~ {NoAI:true,Silent:true,Invulnerable:true,ActiveEffects:[{Id:14,Duration:111110,ShowParticles:false}],Age:-1000000,CustomName:sneak}
  4. /execute @p ~ ~ ~ /tp @e[name=sneak] @a[score_control_min=1]
  5. /tp @e[name=sneak] ~0.01 ~ ~
  6. /execute @a[score_control_min=1,score_sneak_min=1] ~ ~ ~ setblock [x] [y] [z] minecrat:redstone_block 0 replace
  7. /scoreboard players reset @a sneak
  8. /scoreboard objectives add sneak stat.sneakTime
复制代码
在(x,y,z)用红石块激活指定模块

  • 另外各种操作
总之就是利用各种方法实现对应事件监听,然后再调用这些操作。
例如右键可以靠胡萝卜钓竿的使用监听,左键可以靠对实体的伤害监听;
数字键123456789可以靠NBT-SelectedItemSlot;
还有等等等等都可以靠各种方法监听。



优化
  • 视角卡顿
如果你真的按照上述方法做了,而且你找到了合适的执行顺序,你会发现一个致命的问题——卡顿
其实这个视角卡顿就是因为模块执行和实际玩家移动检测存在时间差,因此在你移动后因为又被tp会原始点,你的视角将疯狂摇晃。
解决方法就是降低玩家移动速度,这就是我们需要强行让玩家穿上一个靴子:
  1. /replaceitem entity @p slot.armor.feet diamond_boots 1 0 {Unbreakable:1,display:{Name:"Exit"},HideFlags:63,AttributeModifiers:[{AttributeName:"generic.movementSpeed",Name:"generic.movementSpeed",Amount:-0.099999,Operation:0,UUIDMost:84265,UUIDLeast:167246,Slot:"feet"}]}
复制代码
降低玩家速度到-0.099999,这样玩家可以存在轻微的移动,但是你看不出来你的位置在变化

  • 视角变小
由于玩家减速,因此玩家视角变小。这没有唯一解决方案,只需要你找到一个合适的创建场景的角度。

  • 实体消失范围
当你所控制的角色里玩家实际位置太远了将不会得到渲染,也就是消失。因此需要注意场景离玩家的最远点尽量不要超过5~6个区块。

  • 关于GUI
你可以使用游戏自带的GUI,那么你可以用
  1. /effect @p[tag=controller] invisibility 1 1 true
复制代码
隐藏玩家的手
你也可以选择让玩家F1隐藏GUI,提升游戏体验性。

  • 退出游戏
你可以靠各种方式制作一个GUI,而Demo的退出方式就是脱下你的靴子。



最终指令

Start 游戏开始部分:
  1. icb:/scoreboard players tag @p add controller
  2. *给玩家添加一个控制者标签
  3. /replaceitem entity @p slot.armor.feet diamond_boots 1 0 {Unbreakable:1,display:{Name:"Exit"},HideFlags:63,AttributeModifiers:[{AttributeName:"generic.movementSpeed",Name:"generic.movementSpeed",Amount:-0.099999,Operation:0,UUIDMost:84265,UUIDLeast:167246,Slot:"feet"}]}
  4. *让玩家穿上前面防卡顿的靴子
  5. /scoreboard objectives add moved dummy
  6. *增加moved计分板
  7. //重置结束
  8. /setblock ~-3 ~1 ~2 minecraft:air 0
  9. /scoreboard objectives add control dummy
  10. /summon minecraft:实体名 [x] [y] [z] {Tags:["character"],NoAI:1,CustomName:"Character",CustomNameVisible:false,Invulnerable:1,Silent:1,Rotation:[180.0f,0.0f]}
  11. *生成角色
  12. /tp @p [x] [y] [z] [dx] [dy]
  13. *将玩家传送到控制器的中心点(也就是"移动监听"中用玩家头颅代表的玩家立足点中心)
  14. /scoreboard players set @e[tag=character] moved 0
  15. *初始化stats
  16. stats entity @e[tag=character] set AffectedEntities @e[tag=character] moved
  17. *stats关联到实体
  18. /gamemode a @p
  19. *设置游戏模式
复制代码

Control 事件监听和操作:
  1. rcb:scoreboard players set @a control 0
  2. scoreboard players set @a control 1 {Inventory:[{id:"minecraft:diamond_boots",Slot:100b,Damage:0s,Count:1b,tag:{display:{Name:"Exit"}}}]}
  3. *检测玩家没有选择离开游戏,给玩家control计分板设为1,表示玩家正在操作
  4. testfor @a[tag=controller,score_control=0,c=1]
  5. cond:testforblock ~-3 ~1 ~4 air 0
  6. cond:setblock ~-4 ~1 ~4 redstone_block 0
  7. *检测玩家退出后,激活End()模块
  8. tp @a[score_control_min=1] ~ ~ ~ [dx] [dy]
  9. execute @e[tag=character,score_moved_min=1] ~ ~ ~ tp @a[score_control_min=1] [x] ~ [z]
  10. cond:scoreboard players set @e[tag=character] moved 0
  11. *固定和重置玩家视角和坐标
  12. execute @a[score_control_min=1] ~ ~ ~ detect ~ ~2 ~-0.49999999999995 air 0 execute @e[tag=character] ~ ~ ~ detect ~ ~1 ~-0.9 air 0 tp @e[tag=character] ~ ~ ~-0.6
  13. execute @a[score_control_min=1] ~ ~ ~ detect ~0.49999999999995 ~2 ~ air 0 execute @e[tag=character] ~ ~ ~ tp @e[tag=character] ~ ~ ~ -90 ~
  14. execute @a[score_control_min=1] ~ ~ ~ detect ~0.49999999999995 ~2 ~ air 0 execute @e[tag=character] ~ ~ ~ detect ~0.9 ~1 ~ air 0 tp @e[tag=character] ~0.6 ~ ~
  15. execute @a[score_control_min=1] ~ ~ ~ detect ~ ~2 ~0.49999999999995 air 0 execute @e[tag=character] ~ ~ ~ detect ~ ~1 ~0.9 air 0 tp @e[tag=character] ~ ~ ~0.6
  16. execute @a[score_control_min=1] ~ ~ ~ detect ~ ~2 ~-0.49999999999995 air 0 execute @e[tag=character] ~ ~ ~ tp @e[tag=character] ~ ~ ~ 180 ~
  17. *在Space操作之前移动操作可使用的顺序
  18. //Space事件
  19. execute @a[score_control_min=1] ~ ~ ~ detect ~ ~2.99999 ~ air 0 execute @p ~ ~ ~ detect [x] [y] [z] air 0 setblock [x] [y] [z] redstone_block 0 replace
  20. //Space事件结束
  21. execute @a[score_control_min=1] ~ ~ ~ detect ~ ~2 ~0.49999999999995 air 0 execute @e[tag=character] ~ ~ ~ tp @e[tag=character] ~ ~ ~ 0 ~
  22. execute @a[score_control_min=1] ~ ~ ~ detect ~-0.49999999999995 ~2 ~ air 0 execute @e[tag=character] ~ ~ ~ detect ~-0.9 ~1 ~ air 0 tp @e[tag=character] ~-0.6 ~ ~
  23. execute @a[score_control_min=1] ~ ~ ~ detect ~-0.49999999999995 ~2 ~ air 0 execute @e[tag=character] ~ ~ ~ tp @e[tag=character] ~ ~ ~ 90 ~
  24. *在Space操作之后移动操作可使用的顺序
  25. //Shift事件
  26. /tp @e[name=sneak] ~ ~-300 ~
  27. /kill @e[name=sneak]
  28. /summon minecraft:rabbit ~ ~ ~ {NoAI:true,Silent:true,Invulnerable:true,ActiveEffects:[{Id:14,Duration:111110,ShowParticles:false}],Age:-1000000,CustomName:sneak}
  29. /execute @p ~ ~ ~ /tp @e[name=sneak] @a[score_control_min=1]
  30. /tp @e[name=sneak] ~0.01 ~ ~
  31. /execute @a[score_control_min=1,score_sneak_min=1] ~ ~ ~ setblock [x] [y] [z] minecrat:redstone_block 0 replace
  32. /scoreboard players reset @a sneak
  33. /scoreboard objectives add sneak stat.sneakTime
  34. //Shift事件结束
  35. //可选配置项目
  36. /effect @p[tag=controller] invisibility 1 1 true
复制代码

End 游戏结束部分:
  1. icb:/clear @a diamond_boots 0 10 {display:{Name:"Exit"}}
  2. /scoreboard objectives remove moved
  3. /scoreboard objectives remove control
  4. *各种重置
  5. /setblock ~-3 ~1 ~-2 minecraft:air
  6. *清除Start()模块开头的红石块
  7. /stats entity @e[tag=character] clear AffectedEntities
  8. /gamemode c @a
  9. *继续各种重置
  10. /tp @a [x] [y] [z] [dx] [dy]
  11. *返回大厅
  12. /tp @e[tag=character] ~ -100 ~
  13. /kill @e[tag=character]
  14. /scoreboard players tag @a remove controller
  15. *再各种重置
复制代码

如果你使用上述指令,那么模块的摆放也有相应要求,具体看"Demo&源码资源"



Demo&源码资源
Demo地址:百度网盘
.pcb文件打包地址:百度网盘

关于.pcb转为OOC后的摆放顺序:


此贴实际研究时间数个月前,而且感谢地图《A Day to Remember》给予的2.5D第三人称操作灵感。[groupid=546]Command Block Logic[/groupid]