第三天:享元模式
森林
MilK:丁丁,有人反应你做的东西太丑了。
丁丁:u1s1,确实
MliK:那你知道该怎么做了吗?
丁丁:嗯,知道...等等,今天还有什么好用的方法吗?我可不想等我做完之后重新修改
MliK(一脸嫌弃):没有,自己想去
丁丁:好吧
MilK:快去加班,弄一个好看点的场景出来看看
丁丁:那就做一个森林吧
迷雾散尽,露出了古朴庄严的森林。古老的松树,编织成绿色的海洋。阳光在树梢之间跳动,从树干间远眺,远处的森林渐渐隐去...
这是游戏开发者梦想的超凡场景,这样的场景通常由一个模式支撑着,它低调至极,以至于我们经常使用它而不知道它叫什么:享元模式。
一句话就能描述一片巨大的森林,但是在cavans画板中做这一片森林就完全是另一回事了,当屏幕上需要显示一个森林时,图形程序员看到的是每秒需要送到GPU六十次的百万多边形。(一般人眼能识别极限频率是每秒60帧)
成千上万的树,每棵都由上千的多边形组成。就算有足够的内存描述这片森林,在渲染的过程中CPU和GPU的压力也太大了。
每棵树都有一些自己的属性:
- 定义树干,树枝和树叶形状的多边形
- 树皮和树叶的纹理
- 在森林中树的位置和朝向
- 大小和颜色之类的调节参数,让每棵树看起来与众不同
- 等等
多边形和纹理的体积非常大,把这么多数据在1/60秒内交给GPU处理实在是太过分了。幸运的是,有一种老办法来处理它。关键点在于,哪怕森林里有千千万万的树,它们大多数长得一摸一样,它们使用了相同的形状和纹理。这意味着这些树的实例大部分字段是一样的。
你要么是疯了,要么是亿万富翁,才能让你的UI给整个森林的每一棵树建立一个独立的模型。
所有相同的属性和参数,我们都可以放在同一个模型中,让其他单元共享。
像IDEA中有代码查重功能和自动修复功能,用封装的方式,尽量减少重复代码的出现(也可以理解为享元)。作为一个程序员,想必这种思想你已经烂熟于心了。当然,除了自己代码,你还要考虑CPU和内存的重复使用,如果你的代码能有效的减少计算机的工作量的话,那你就步入资深程序员了。
style和src
作为一个前端程序员,我们不妨看看前端哪里有用到“享元模式”。
哈哈,想必你看标题就已经知道了。在css中,我们常用的class,就利用了享元的思想。另外,当你在用src获取资源的时候,即使页面上多次使用同样的链接,实际上浏览器也只会加载一次,也是享元的思想(第一次调用之后,这个资源就会存在缓存池中,浏览器会优先从缓存池中寻找静态资源,缓存池中没有再从互联网获取)。
<style type="text/css">
.class1{background-color: #FFFFEE;color: #000000}
</style>
<div >
<div class="class1">1</div>
<div class="class1">2</div>
<div class="class1">3</div>
<img src="img/1.jpg">
<img src="img/1.jpg">
<img src="img/1.jpg">
</div>
这里的class1和1.jpg就是被共享的元。
看到这里你会说:这也太简单了吧,有什么好讲的~
设计模式注重的是我们时刻不要忘记这种思想,不管在写什么代码的时候,都不要忘记这种思路。(享元其实是对象池,线程池,连接池,常量池等...是资源共享的高级应用,也有人说: 享元模式 !== 资源共享)
好吧,不装了,我摊牌了,我也觉得没什么好讲的,所以这一章我们就专心打造我们的“森林”吧。
透视
这是一个美术上的东西,我高中的时候想要艺考还特地向MilK拜师学过。现在我们就要用计算机简单模拟一下透视的效果。
值得注意的是我们眼睛的位置,屏幕的位置,以及物体实际的位置。我们假设位置关系是这样的:
世界的坐标系(x, y, z):- 眼睛的位置(0, 200, 0)
- 屏幕的位置(x, y, 200)
- 物体中心点的位置(0,90,700)
- 物体的位置(-50, 0, 700)~(50, 180, 700)
通过相似三角形的原理
易证,屏幕中的坐标系(x,y):- 物体中心点在屏幕上的投影:(物体X坐标0 * (屏幕距离200 / 物体距离700), 眼睛高度200 + (物体高度90 - 眼睛高度200) * ( 屏幕距离200 / 物体距离700)})
- => (0, 168.5)
- 物体的在屏幕上的投影:(-50 * (200 / 700), 200 + (0 - 200) * (200 / 700)) ~ (50 * (200 / 700), 200 + (180 - 200) * (200 / 700))
- => (-14.28, 142.85) ~ ( 14.28, 194.28)
得出公式:
- 透视缩放比例 = 屏幕z坐标 / 物体z坐标
- 物体中心点y坐标 = 眼睛y坐标 +(物体y坐标 - 眼睛y坐标)* 透视缩放比例
- 物体中心点x坐标 = 物体x坐标 * 透视缩放比例
- 物体中心点x坐标 = 眼睛x坐标 +(物体x坐标 - 眼睛x坐标)* 透视缩放比例 (眼睛x坐标不为0的话)
所以为了实现透视的效果,我们必须定义好屏幕到眼睛的距离和眼睛的位置参数:
const param = {
screenZ : 200,//屏幕到眼睛的距离
eyeY : 200,//眼睛的高度
eyeX : 0,//眼睛的x坐标
};
编写计算css的函数:(直接写在unit的构造函数中,以屏幕中心点为x和y的零点)
function CreateUnit (unit){
this.width = unit.width || 1000;//宽度
this.height = unit.height || 1000;//高度
this.left = unit.left || 100;//最终渲染X坐标
this.top = unit.top || 100; //最终渲染Y坐标
this.proportion = unit.proportion || 1;//缩放比例
this.x = unit.x || 0;//虚拟X坐标
this.y = unit.y || 0;//虚拟Y坐标
this.z = unit.z || 1700;//虚拟Z坐标
this.v = unit.v || 100;//移动速度
this.move = function (left,top) {//移动函数
this.left = left;
this.top = top;
}
this.updateStyle = function () {//根据xyz坐标,计算样式
if(this.z < param.screenZ){
this.z = param.screenZ;
}
this.proportion = param.screenZ / this.z;
this.left = window.innerWidth / 2 + param.eyeX + (this.x - param.eyeX) * this.proportion;
this.top = window.innerHeight / 2 - param.eyeY - (this.y - param.eyeY) * this.proportion;
}
}
export default CreateUnit;
修改移动命令:(左右移动是改变x坐标,上下移动是改变z坐标)
/**
* 向右移动命令
* @constructor
*/
export function goRightCommand (received){
let beforeX = received.x;//记录原先的X坐标
return {
execute : function goRight() {//向右移动函数
received.x = received.x + received.v;
received.updateStyle();
},
undo : function () {//撤销函数
received.x = beforeX;
received.updateStyle();
}
}
}
/**
* 向上移动命令
* @constructor
*/
export function goUpCommand (received){
let beforeZ = received.z;//记录原先的Z坐标
return {
execute : function goRight() {//向右移动函数
received.z = received.z + received.v;
received.updateStyle();
},
undo : function () {//撤销函数
received.z = beforeZ;
received.updateStyle();
}
}
}
定义好Tree的类和Img组件:
import CreateUnit from "@/pages/baseComponent/Unit";
import treeImg from "@/img/tree.png"
function Tree (tree){
let flag=tree==null;
this.name=flag?"tree":tree.name||"tree";
this.src=flag?treeImg:tree.src||treeImg;
}
Tree.prototype = new CreateUnit({});
export default Tree;
class ImgUnit extends Component{
render() {
const {onClick,unit}=this.props;
return <div
onClick = {onClick}
>
<img
src = {unit.src} alt={"img"}
style = {{
width:unit.width * unit.proportion,
height:unit.height * unit.proportion,
left:unit.left- unit.width * unit.proportion / 2,
top:unit.top- unit.height* unit.proportion / 2,
position:"absolute",
textAlign:"center",
lineHeight:unit.height+"px"
}}
/>
</div>;
}
}
接下来就是激动人心的效果时间:
多放几棵可爱的松树: 怎么样,看起来还不错吧优化
其实这样的代码还是有问题的:
发现问题了吗,数组后面的树会永远在前面的树上面显示,所以我们在渲染之前还需要对数组进行一次排序,让近的物体在远的物体前面:
/**
* REACT生命周期主函数,用于渲染页面
* @returns {*}
*/
render() {
this.treeList.sort(function (a,b) {//重新排序
return b.z - a.z;
});
return (
<div className = {"game3"}>
{
this.treeList.map((item,i)=>{
return <ImgUnit
key = {i}
onClick = {()=>{
this.setFocus(item);
}}
unit = {item}
/>
})
}
</div>
);
}
效果:
噢,不过每一次渲染都要重新排序真不是一个好办法,能不能在某个单元Z轴更改的同时修改顺序呢?
答案是肯定的,我就有一个好办法,不过在这里我先卖个关子,之后在碰撞检测的部分会详细说明,大家可以先自己思考思考,欢迎在评论下方交流哦~
加班
你做的森林一点也不好看!改好看一点!