这个系列通过对我的世界Minecraft源码进行拆分讲解,让大家可以清除的了解一款游戏是怎么一步步被实现出来的,下面就介绍Minecraft源码第一篇内容,关于刷怪逻辑。

生成循环

生物大致划分为四种:攻击型,被动型,水生型(也就是鱿鱼)和环境型(也就是蝙蝠)。攻击型生物每一个Tick(1/20秒)有一次的生成周期。被动型和水生型生物只有每400刻(20秒)一次的生成周期。因为这一点,攻击型生物可以在任何时候生成,而动物生成则非常少。另外,大部分动物在建立世界时产生的Block中生成。

生物的刷新范围通常是以玩家为中心的 15 * 15的Chunk,当有多个玩家的时候,所有玩家的附近都会刷新。当敌对怪物离玩家超过128个block的时候,就会立即把它刷掉。下图表述刷新逻辑和玩家距离的关系。

Entity种类

源码中对应的类是

EnumCreatureType.java

publicenumEnumCreatureType

{

MONSTER(IMob.class,70, Material.air,false,false),

CREATURE(EntityAnimal.class,10, Material.air,true,true),

AMBIENT(EntityAmbientCreature.class,15, Material.air,true,false),

WATER_CREATURE(EntityWaterMob.class,5, Material.water,true,false);

/**

* The root class of creatures associated with this EnumCreatureType (IMobs for aggressive creatures, EntityAnimals

* for friendly ones)

*/

privatefinalClass creatureClass;

privatefinalintmaxNumberOfCreature;

privatefinalMaterial creatureMaterial;

/** A flag indicating whether this creature type is peaceful. */

privatefinalbooleanisPeacefulCreature;

/** Whether this creature type is an animal. */

privatefinalbooleanisAnimal;

privateEnumCreatureType(Classclass,int_maxNumberOfCreature, Material _creatureMaterial,boolean_isPeacefulCreature,boolean_isAnimal)

{

this.creatureClass =class;

this.maxNumberOfCreature = _maxNumberOfCreature;

this.creatureMaterial = _creatureMaterial;

this.isPeacefulCreature = _isPeacefulCreature;

this.isAnimal = _isAnimal;

}

publicClass getCreatureClass()

{

returnthis.creatureClass;

}

publicintgetMaxNumberOfCreature()

{

returnthis.maxNumberOfCreature;

}

/**

* Gets whether or not this creature type is peaceful.

*/

publicbooleangetPeacefulCreature()

{

returnthis.isPeacefulCreature;

}

/**

* Return whether this creature type is an animal.

*/

publicbooleangetAnimal()

{

returnthis.isAnimal;

}

}怪物容量(可理解为人口)与适合生成的Chunk总数直接成比例。要计算容量的话,生成区域在每一方向上均扩展一个区块,所以有17*17 BChunk的大小,然后总的ChunkCount被代入到下式中:容量 = 常量* ChunkCount / 289每一种生物均具有自己的容量计算和公式中不同的常量值:攻击型 = 70被动型 = 10环境型(蝙蝠) = 15

水生型 = 5

在单人游戏的时候,ChunkCount 一直是289,但是在多人游戏中,每个chunk只被计算一次,所以玩家分得越开,怪物的容量也就越大。

在 Spawn开始之前,首先进行一次 mob 容量检查,如果生物的数量超过了容量,那么这个种类的生物的生成就会跳过。

服务器架构

图1. MC服务器架构

MinecraftServer作为服务器,主要负责服务端的更新,里面可以包含多个WorldServer,WorldClent作为服务端,当玩家加入一个服务器的时候,就会创建一个在本地。SpawnerAnimals作为刷怪的工具类,主要用来处理刷怪逻辑。

首先来看WorldServer的Tick

注:如无特别说明,所有tick都是一秒20次。

图2. WorldServer Tick 流程

很清晰的逻辑,这里主要看一下findChunksForSpawning的实现。

在单人游戏模式下,区块计数总为17*17=289,那么各种生物的容量也就是上面列出的数值。在多人游戏中,在多个玩家范围内的区块只被计算一次,所以玩家越分散,更多地区块会被覆盖且会有更高的生物容量。

在每次生成周期的开始都会检查一次容量。如果存活的生物数量超过它的容量,整个生成周期就会被跳过。

在每一生成周期中,会在每一个合适的区块中进行一次生成一组生物的尝试。该区块内选择一个随机地点作为这组生物的中心点。为生成这组生物,中心方块对水生生物而言必须是水方块,对所有其它生物来说则必须是空气方块。注意在后面的情形中,它一定得是空气方块。任何其它方块,哪怕是一个透明方块都会阻止整组生物的生成。

图3. 陆地怪物的生成条件

如果该组位置合适,会在以中心方块为原点41*1*41的范围(就是41*41格大小的方型,有1格高的区域)内进行12次尝试以生成多至4个的生物(狼是8个,恶魂是1个)。生物将会在这一区域生成其身体的最下部分。在每次生成尝试中,会在这一区域中随机选择一个方块的地点。尽管生成区域能扩展到中心21格之外,但是随机选出的地点强烈地向该组的中心集中。大约有85%的生成将会在该组中心的5格以内,99%会落在10格以内

组内所有的生物都是相同的种类。在该组第一次生成尝试时从该地区所适合生成的种类中随机挑选一种以决定整组的种类。

具体的种类可以参考要Minecraft的Wiki。

findChunksForSpawning函数实现的就是上面描述的逻辑。看一下SpawnerAnimals.java这个类。

publicfinalclassSpawnerAnimals

{

privatestaticfinalintMOB_COUNT_DIV = (int)Math.pow(17.0D,2.0D);

/** The 17x17 area around the player where mobs can spawn */

privatefinalSet eligibleChunksForSpawning = Sets.newHashSet();

privatestaticfinalString __OBFID ="CL_00000152";

/**

* adds all chunks within the spawn radius of the players to eligibleChunksForSpawning. pars: the world,

* hostileCreatures, passiveCreatures. returns number of eligible chunks.

*/

publicintfindChunksForSpawning(WorldServer server,booleanspawnHostileMobs,booleanspawnPeacefulMobs,booleanisSpecialSpawnTick)

{

if(!spawnHostileMobs && !spawnPeacefulMobs)

{

return0;

}

else

{

this.eligibleChunksForSpawning.clear();

intchunkCount =0;

Iterator iterator = server.playerEntities.iterator();

intk;

intcreatureCount;

while(iterator.hasNext())

{

EntityPlayer entityplayer = (EntityPlayer)iterator.next();

if(!entityplayer.isSpectator())

{

intchunkCoordX = Mathf.FloorToInt(entityplayer.GetPosition().x /16.0f);

intchunkCoordZ = Mathf.FloorToInt(entityplayer.GetPosition().z /16.0f);

bytechunkLength =8;

for(intdirectionX = -chunkLength; directionX <= chunkLength;  directionX)

{

for(directionZ = -chunkLength; directionZ <= chunkLength;  directionZ)

{

bool isBorderChunk = (directionX == -chunkLength || directionX == chunkLength || directionZ == -chunkLength || directionZ == chunkLength);

IntVector2 chunkcoordintpair = newIntVector2(directionX   chunkCoordX, directionZ   chunkCoordZ);

if(!this.eligibleChunksForSpawning.Contains(chunkcoordintpair))

{

chunkCount;

if(!isBorderChunk && server.getWorldBorder().contains(chunkcoordintpair))

{

this.eligibleChunksForSpawning.Add(chunkcoordintpair);

}

}

}

}

}

}

inttotalEntityCount =0;

BlockPos blockpos2 = server.getSpawnPoint();

EnumCreatureType[] aenumcreaturetype = EnumCreatureType.values();

k = aenumcreaturetype.length;

for(inti =0; i

{

EnumCreatureType enumcreaturetype = aenumcreaturetype[i];

if((!enumcreaturetype.getPeacefulCreature() || spawnPeacefulMobs) && (enumcreaturetype.getPeacefulCreature() || spawnHostileMobs) && (!enumcreaturetype.getAnimal() || isSpecialSpawnTick))

{

creatureCount = server.countEntities(enumcreaturetype, true);

intmaxCreatureCount = enumcreaturetype.getMaxNumberOfCreature() * chunkCount / MOB_COUNT_DIV;

if(creatureCount <= maxCreatureCount)

{

Iterator iterator1 = this.eligibleChunksForSpawning.iterator();

ArrayListtmp = newArrayList(eligibleChunksForSpawning);

Collections.shuffle(tmp);

iterator1 = tmp.iterator();

label115:

while(iterator1.hasNext())

{

ChunkCoordIntPair chunkcoordintpair1 = (ChunkCoordIntPair)iterator1.next();

BlockPos blockpos = getRandomChunkPosition(server, chunkcoordintpair1.chunkXPos, chunkcoordintpair1.chunkZPos);

intj1 = blockpos.getX();

intk1 = blockpos.getY();

intl1 = blockpos.getZ();

Block block = server.getBlockState(blockpos).getBlock();

if(!block.isNormalCube())

{

intentityCountOnChunk =0;

intj2 =0;

while(j2 <3)

{

intk2 = j1;

intl2 = k1;

inti3 = l1;

byteb1 =6;

BiomeGenBase.SpawnListEntry spawnlistentry = null;

IEntityLivingData ientitylivingdata = null;

intj3 =0;

while(true)

{

if(j3 <4)

{

label108:

{

k2  = server.rand.nextInt(b1) - server.rand.nextInt(b1);

l2  = server.rand.nextInt(1) - server.rand.nextInt(1);

i3  = server.rand.nextInt(b1) - server.rand.nextInt(b1);

BlockPos blockpos1 = newBlockPos(k2, l2, i3);

floatf = (float)k20.5F;

floatf1 = (float)i30.5F;

//Check must be away from Player by 24 block, and away from player spawn point. 576 = 24 * 24

if(!server.CheckCanSpawnHere((double)f, (double)l2, (double)f1,24.0D) && blockpos2.distanceSq((double)f, (double)l2, (double)f1) >=576.0D)

{

if(spawnlistentry ==null)

{

spawnlistentry = server.GetSpawnListEntry(enumcreaturetype, blockpos1);

if(spawnlistentry ==null)

{

breaklabel108;

}

}

if(server.CheckChunkHasSpawnEntry(enumcreaturetype, spawnlistentry, blockpos1) && canCreatureTypeSpawnAtLocation(EntitySpawnPlacementRegistry.GetSpawnPointType(spawnlistentry.entityClass), server, blockpos1))

{

EntityLiving entityliving;

try

{

entityliving = (EntityLiving)spawnlistentry.entityClass.getConstructor(newClass[] {World.class}).newInstance(newObject[] {server});

}

catch(Exception exception)

{

exception.printStackTrace();

returntotalEntityCount;

}

entityliving.setLocationAndAngles((double)f, (double)l2, (double)f1, server.rand.nextFloat() *360.0F,0.0F);

Result canSpawn = ForgeEventFactory.canEntitySpawn(entityliving, server, f, l2, f1);

if(canSpawn == Result.ALLOW || (canSpawn == Result.DEFAULT && (entityliving.getCanSpawnHere() && entityliving.handleLavaMovement())))

{

if(!ForgeEventFactory.doSpecialSpawn(entityliving, server, f1, l2, f1))

ientitylivingdata = entityliving.getEntityData(server.getDifficultyForLocation(newBlockPos(entityliving)), ientitylivingdata);

if(entityliving.handleLavaMovement())

{

entityCountOnChunk;

server.spawnEntityInWorld(entityliving);

}

if(entityCountOnChunk >= ForgeEventFactory.getMaxSpawnPackSize(entityliving))

{

continuelabel115;

}

}

totalEntityCount  = entityCountOnChunk;

}

}

j3;

continue;

}

}

j2;

break;

}

}

}

}

}

}

}

returntotalEntityCount;

}

}

protectedstaticBlockPos getRandomChunkPosition(World worldIn,intx,intz)

{

Chunk chunk = worldIn.getChunkFromChunkCoords(x, z);

intk = x *16worldIn.rand.nextInt(16);

intl = z *16worldIn.rand.nextInt(16);

intcreatureCount = MathHelper.Ceiling(chunk.getHeight(newBlockPos(k,0, l))1,16);

intj1 = worldIn.rand.nextInt(creatureCount >0? creatureCount : chunk.getTopFilledSegment()16-1);

returnnewBlockPos(k, j1, l);

}

publicstaticbooleancanCreatureTypeSpawnAtLocation(EntityLiving.SpawnPlacementType placeType, World worldIn, BlockPos pos)

{

if(!worldIn.getWorldBorder().contains(pos))

{

returnfalse;

}

else

{

Block block = worldIn.getBlockState(pos).getBlock();

if(placeType == EntityLiving.SpawnPlacementType.IN_WATER)

{

returnblock.getMaterial().isLiquid() && worldIn.getBlockState(pos.down()).getBlock().getMaterial().isLiquid() && !worldIn.getBlockState(pos.up()).getBlock().isNormalCube();

}

else

{

BlockPos blockpos1 = pos.down();

if(!worldIn.getBlockState(blockpos1).getBlock().canCreatureSpawn(worldIn, blockpos1, placeType))

{

returnfalse;

}

else

{

Block block1 = worldIn.getBlockState(blockpos1).getBlock();

booleanflag = block1 != Blocks.bedrock && block1 != Blocks.barrier;

returnflag && !block.isNormalCube() && !block.getMaterial().isLiquid() && !worldIn.getBlockState(pos.up()).getBlock().isNormalCube();

}

}

}

}

/**

* Called during chunk generation to spawn initial creatures.

*/

publicstaticvoidperformWorldGenSpawning(World worldIn, BiomeGenBase biomeGenBase,intchunkCenterX,intchunkCenterY,intrangeX,intrangeY, Random rand)

{

List list = biomeGenBase.getSpawnableList(EnumCreatureType.CREATURE);

if(!list.isEmpty())

{

while(rand.nextFloat()

{

BiomeGenBase.SpawnListEntry spawnlistentry = (BiomeGenBase.SpawnListEntry)WeightedRandom.getRandomItem(worldIn.rand, list);

intcreatureCount = spawnlistentry.minGroupCount   rand.nextInt(1spawnlistentry.maxGroupCount - spawnlistentry.minGroupCount);

IEntityLivingData ientitylivingdata = null;

intj1 = chunkCenterX   rand.nextInt(rangeX);

intk1 = chunkCenterY   rand.nextInt(rangeY);

intl1 = j1;

intentityCountOnChunk = k1;

for(intj2 =0; j2

{

booleanflag =false;

for(intk2 =0; !flag && k2 <4;  k2)

{

BlockPos blockpos = worldIn.getTopSolidOrLiquidBlock(newBlockPos(j1,0, k1));

if(canCreatureTypeSpawnAtLocation(EntityLiving.SpawnPlacementType.ON_GROUND, worldIn, blockpos))

{

EntityLiving entityliving;

try

{

entityliving = (EntityLiving)spawnlistentry.entityClass.getConstructor(newClass[] {World.class}).newInstance(newObject[] {worldIn});

}

catch(Exception exception)

{

exception.printStackTrace();

continue;

}

entityliving.setLocationAndAngles((double)((float)j10.5F), (double)blockpos.getY(), (double)((float)k10.5F), rand.nextFloat() *360.0F,0.0F);

worldIn.spawnEntityInWorld(entityliving);

ientitylivingdata = entityliving.func_180482_a(worldIn.getDifficultyForLocation(newBlockPos(entityliving)), ientitylivingdata);

flag = true;

}

j1  = rand.nextInt(5) - rand.nextInt(5);

for(k1  = rand.nextInt(5) - rand.nextInt(5); j1 = chunkCenterX   rangeX || k1 = chunkCenterY   rangeX; k1 = entityCountOnChunk   rand.nextInt(5) - rand.nextInt(5))

{

j1 = l1   rand.nextInt(5) - rand.nextInt(5);

}

}

}

}

}

}

}

生物群落的生成

在维基百科中,生物群系(Biome)在气候学和地理学上被定义为具有类似气候条件的地方,比如植物、动物和土壤生物组成的群落,它经常被称作生态系统。是Minecraft里有不同的地域特色,植物,高度,温度,湿度评级的地区。 在Minecraft中, 从万圣节更新开始,它意味着具有不同高度、温度、湿度、叶子颜色的区域。

图4. 高寒地区Biome

图5. 雪地Biome

当地图被创建时会具有雪地或草地主题。但在这个更新之后,一个世界中就可以具有所有的主题,它们的分布由生物群系图决定。

Anvil文件格式中,世界数据直接存储在生物群系中,这不同于先前的国家或地区的文件格式的格式,其中的生物群系,从种子中动态计算。

Chunk中的生物也是根据Biome来生成,这里的限制主要体现在当SpawnAnimals在Chunk中生成生物的时候,总会调用WorldServer的方法进行Check,看对应的生物种类能否生成。

publicbooleanCheckCreatureCanSpawn(EnumCreatureType creatureType, BiomeGenBase.SpawnListEntry spawnListEntry, BlockPos blockPos)

{

List list = this.getChunkProvider().getSpawnableList(creatureType, blockPos);

returnlist !=null&& !list.isEmpty() ? list.contains(spawnListEntry) :false;

}

相关的还有ChunkProviderGenerate.getSpawnableList()

publicList getSpawnableList(EnumCreatureType creatureType, BlockPos pos)

{

BiomeGenBase biomegenbase = this.worldObj.getBiomeGenForCoords(pos);

if(this.mapFeaturesEnabled)

{

if(creatureType == EnumCreatureType.MONSTER &&this.scatteredFeatureGenerator.func_175798_a(pos))

{

returnthis.scatteredFeatureGenerator.getScatteredFeatureSpawnList();

}

if(creatureType == EnumCreatureType.MONSTER &&this.settings.useMonuments &&this.oceanMonumentGenerator.func_175796_a(this.worldObj, pos))

{

returnthis.oceanMonumentGenerator.func_175799_b();

}

}

returnbiomegenbase.getSpawnableList(creatureType);

}

而所有的Biome和生物的对应关系都在BiomeGenBase这个类中定义。

在EntityLiving中定义了一个getCanSpawnHere函数,用于查询是否可以在某个位置生成

/**

* Checks if the entity's current position is a valid location to spawn this entity.

*/

publicbooleangetCanSpawnHere()

{

returntrue;

}继承EntityLiving的类就override这个函数,比如EntityAnimal

/**

* Checks if the entity's current position is a valid location to spawn this entity.

*/

publicbooleangetCanSpawnHere()

{

inti = MathHelper.floor_double(this.posX);

intj = MathHelper.floor_double(this.getEntityBoundingBox().minY);

intk = MathHelper.floor_double(this.posZ);

BlockPos blockpos = newBlockPos(i, j, k);

returnthis.worldObj.getBlockState(blockpos.down()).getBlock() ==this.spawnBlock &&this.worldObj.getLight(blockpos) >8&&super.getCanSpawnHere();

}史莱姆的

/**

* Checks if the entity's current position is a valid location to spawn this entity.

*/

publicbooleangetCanSpawnHere()

{

Chunk chunk = this.worldObj.getChunkFromBlockCoords(newBlockPos(MathHelper.floor_double(this.posX),0, MathHelper.floor_double(this.posZ)));

if(this.worldObj.getWorldInfo().getTerrainType().handleSlimeSpawnReduction(rand, worldObj))

{

returnfalse;

}

else

{

if(this.worldObj.getDifficulty() != EnumDifficulty.PEACEFUL)

{

BiomeGenBase biomegenbase = this.worldObj.getBiomeGenForCoords(newBlockPos(MathHelper.floor_double(this.posX),0, MathHelper.floor_double(this.posZ)));

if(biomegenbase == BiomeGenBase.swampland &&this.posY >50.0D &&this.posY <70.0D &&this.rand.nextFloat() <0.5F &&this.rand.nextFloat()

{

returnsuper.getCanSpawnHere();

}

if(this.rand.nextInt(10) ==0&& chunk.getRandomWithSeed(987234911L).nextInt(10) ==0&&this.posY <40.0D)

{

returnsuper.getCanSpawnHere();

}

}

returnfalse;

}

}

参考

Minecraft Wiki

Minecraft Forge

http://blog.csdn.net/silangquan/article/details/51258052

我的世界java刷怪数量_我的世界Minecraft源码分析(1):刷怪逻辑相关推荐

  1. Minecraft源码分析(3) - 刷怪笼(MobSpawner)逻辑

    Minecraft刷怪笼顾名思义就是刷怪的笼子,遍布Minecraft各地,除了在水中.浮空和末地不存在外,一般来说刷怪笼都存在于低于地平线的位置,但是也有一些刷怪笼生成于高于地面的山中,有时会出现多 ...

  2. Java并发编程笔记之 CountDownLatch闭锁的源码分析

    转 自: Java并发编程笔记之 CountDownLatch闭锁的源码分析 ​ JUC 中倒数计数器 CountDownLatch 的使用与原理分析,当需要等待多个线程执行完毕后在做一件事情时候 C ...

  3. java直接内存为什么快_直接内存与 JVM 源码分析

    直接内存(堆外内存) 直接内存有一种叫法,堆外内存. 直接内存(堆外内存)指的是 Java 应用程序通过直接方式从操作系统中申请的内存.这个差别与之前的堆.栈.方法区,那些内存都是经过了虚拟化.所以严 ...

  4. 【Java】NIO中Selector的select方法源码分析

    该篇博客的有些内容和在之前介绍过了,在这里再次涉及到的就不详细说了,如果有不理解请看[Java]NIO中Channel的注册源码分析, [Java]NIO中Selector的创建源码分析 Select ...

  5. 【Java入门提高篇】Day26 Java容器类详解(八)HashSet源码分析

    前面花了好几篇的篇幅把HashMap里里外外说了个遍,大家可能对于源码分析篇已经讳莫如深了.别慌别慌,这一篇来说说集合框架里最偷懒的一个家伙--HashSet,为什么说它是最偷懒的呢,先留个悬念,看完 ...

  6. Java中ConcurrentHashMap底层实现原理(JDK1.8)源码分析2

    https://blog.csdn.net/programmer_at/article/details/79715177 https://blog.csdn.net/qq_41737716/categ ...

  7. Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法

    文章目录 概述 复现问题 源码分析 How to Fix ? 每次使用时new一个SimpleDateFormat的实例 加锁 使用ThreadLocal 换API - JodaTime or JDK ...

  8. 3 v4 中心节点固定_死磕以太坊源码分析之p2p节点发现

    死磕以太坊源码分析之p2p节点发现 在阅读节点发现源码之前必须要理解kadmilia算法,可以参考:KAD算法详解. 节点发现概述 节点发现,使本地节点得知其他节点的信息,进而加入到p2p网络中. 以 ...

  9. java使用websocket,并且获取HttpSession,源码分析

    一:本文使用范围 此文不仅仅局限于spring boot,普通的spring工程,甚至是servlet工程,都是一样的,只不过配置一些监听器的方法不同而已. 本文经过作者实践,确认完美运行. 二:Sp ...

最新文章

  1. 【OpenCV】5种图像滤波辨析:方框、均值、高斯、中值、双边
  2. c语言调用系统当前时间的函数,c语言获取系统当前时间的函数,求讲解?
  3. 什么时候必须使用UI相机? 多个相机的作用原理?
  4. Springboot 常见请求方式
  5. docker安装(一)
  6. 阿里腾讯都在做的可视化大屏,我用这工具里的100个模板实现了
  7. clojure 使用Lazy-seq创建斐波那契数列
  8. 【Python】Python简介
  9. 基恩士光纤传感器怎么恢复出厂设置_光纤故障排查测试必备神器
  10. 关于if...else语句的小注意
  11. python3GUI--刷屏器(附源码)
  12. Charles 弱网测试
  13. groovy教程入门
  14. 海外抖音推荐算法,玩转tiktok短视频内容运营
  15. 复旦退休教授直播间吸烟被举报:此前因违规吸烟被学校要求做检讨
  16. C语言打印菱形超详细版本,逐句解析
  17. 永洪科技何春涛:中国企业数据技术的6大需求和解决之道
  18. c# rar解压大小_C#解压、压缩RAR文件
  19. JAVA-IDEA开发10个小手段
  20. 凯利讯讲解为什么MOS管可以防止电源反接?

热门文章

  1. c语言程序设计分段定时器,单片机C语言程序设计:按键控制定时器选播多段音乐...
  2. 记MAVEN技巧 用maven坐标从公司nexus私有库上获取所在存储位置
  3. PHP7 学习笔记(八)JetBrains PhpStorm 2017.1 x64 MySQL数据库管理工具的使用
  4. 使用JustDecompile修改程序集
  5. arcgis server for flex中文乱码问题(转)
  6. 【Keras学习】常见问题与解答
  7. linux 新建用户和权限分配
  8. 数学图形(2.8)Viviani曲线
  9. asd.equals(s)与s.equals(asd)为什么前者可避免NullPointerException
  10. 在场景中添加光线——在Deferred Shading引擎中添加阴影能力