今天逆战发布新版了。我做的新军衔系统和转生系统在这次上线了。哈哈,好有成就感。第一次写的代码能够影响这么多人的生活。听说玩家都挤在主城转生NPC处了。
新军衔系统:
转生系统:
STEP BY STEP
在逆战的界面开发中,一般只需要从uc往flash中单向传输数据,因此这里只总结从uc->flash的数据流动方法。flash->uc的数据流动,其实有相对应的方式,比如SetVariableBool函数有对应的GetVariableBool函数。
在unrealscript中,使用scaleform的界面类,都继承自GfxMoviePlayer。
一、传输变量
native function SetVariableString(string path, string s);
native function SetVariableBool(string path, bool b);
native function SetVariableNumber(string path, float f);
我在GfxUIMovie中没找到专门设置int的函数,估计可以用SetVariableNumber替代。关键是理解path的设置,比如对应flash文件的名称是ReBornGuide。那么path可能是ReBornGuide.str或者_root.ReBornGuide.str,具体跟ue3项目的一些设置有关。
这几个函数对应的cpp声明如下:
String:SetVariableString(const FString path, const FString s);
Bool:SetVariableBool(const FString path, UBool b);
Float:SetVariableNumber(const FString path, float f);
有趣的是,这几个函数的字符串参数都是引用,这样恰恰说明了在uc中,字符串是引用类型。
例子:
1 | test.uc中: |
二、传输数组
native function bool SetVariableStringArray(string path, int index, array
native function bool SetVariableFloatArray(string path, int index, array
native function bool SetVariableIntArray(string path, int index, array
其对应的cpp实现
String:SetVariableStringArray(const FString path, INT Index,const TArray
Float:SetVariableFloatArray(const FString path, INT Index,const TArray
Int:SetVariableFloatArray(const FString path, INT Index,const TArray
这里Index表示从数组的第几个元素开始,0表示传输全部,arg是要传输的数组。
例子:
1 | test.uc中: |
三、传输复合对象(结构体)
当需要传输整体的数据,或者传输多个数据的时候,可以将数据组合成一个结构体。假设在uc中定义了如下结构体,
1 | struct RankData |
我们想将其一次性传入as中定义的Object对象,var RD:Object。这里我们可以使用SetVariableObject函数。其定义如下:
native function SetVariableObject(string path, GFxObject Object);
下面的例子很好说明了如何使用该函数。
1 | uc中: |
从上面代表可以看出,GFxObject可以代表一个符号变量,可以设置一个GFxObject的多个属性,然后一次性将这个对象传入flash。GFxObject.uc中有如下Set变量的函数:
1 | native final function SetBool(string Member, bool b); |
因此,可以看出可以设置GFxObject各种类型的属性。关键的是还可以设置其GFxObject类型的属性,从而可以嵌套定义。比如结构体中定义另一结构体成员。当需要将多次传输组合为一次时候,也可以使用这种方式,并不需要一定定义结构体。
四、传输结构体数组
假设as中定义如下数组,var RankListDatas:Array = new Array();那么可以用下面代码传输:
1 | //军衔列表每项的数据 |
从代码里面可以看到,还是用SetVariableObject传输数据。不过,需要用CreateArray()创建GFxObject对象。在as代码中RankListDatas[0].Name就可以访问第一个元素的Name属性了。
GfxRankListDatas.SetElementObject(Index, GfxTempObj)则是将GfxTempObj设置为GfxRankListDatas代表的数组的第Index个元素。
五、传输结构体数组的数组(结构体内部复合结构体数组)
1 | struct AwardData |
从上面代码可以看出,调用GfxTempObj.SetObject(“RankAward”, GfxAwardDatas)可以设置GFxObject对象的GFxObject属性,从而能够传输复合结构体类型的数组。假设as中对应的数组定义为var RankAwardDatas:Array=new Array();那么,在as代码中RankAwardDatas[0].RankAward.IconSource则能访问该数组第一个元素的结构体成员的IconSource属性。
我不知道现在的win8和win10系统上是不是自动显示tga格式的缩略图的。我的win7系统确实有时候显示不了。为什么有这样的需求了,因为我现在的工作是开发游戏,虽然这几个月在写界面。在网上搜到有一个这样的软件MysticThumbs。这显然是外国友人做的东西,肯定是要注册的,官网上面下载的肯定是的,要交钱的。伟大的天朝人民肯定有破解版本了。
下面分享下我下载到的破解版本,里面有使用说明,MysticThumbs破解版本。虽然说里面有破解说明,既然写了这篇文章,我就是想更简略的说明下如何使用。
1.打开之后,直接运行MysticThumbs3.0.0.Multi+Pt FULL。假设安装到默认路径。
2.打开安装路径C:\Program Files\MysticCoder\MysticThumbs,你会看到里面有几个dll。
3.把安装文件里面的patch目录下的文件,复制到2所在的目录即可。
破解完毕,现在不用担心使用一个月后失效了。
这个破解版本在2个多月后失效了,慎用。
7月26号封闭培训,到部门来已经是8月10号了。刚开始懵懵懂懂的,不知道干啥。部门上课的那几天,把新人手册的步骤做了一遍。悲伤的是,做完之后不少忘记了。编译脚本的addin插件至今没有显示出来,虽然有几天它显示过。除此之外,还有不少纠结过的问题。让我一一道来吧。配置vs字体和颜色,va快捷键,查找快捷键无效,F12查看定义无效,uc断点无效,调试uc时候游戏进程掉了。。。下面来说说成功解决掉的几个问题吧,非常感谢jerry哥和vivin姐的帮忙。
1.vs的字体和颜色。默认字体是新宋体,这个字体据说不是很好,推荐consolas字体。颜色的话,有个网站叫做https://studiostyl.es/,
可以去里面选自己喜欢的风格,设置好就行。值得注意的是,安装nfringe后,字体颜色设置面板里面会出现unrealscript相关的设置,注意修改。
如图所示,
另一个需要注意的地方是,与.uc文件不同,.cpp中的代码颜色是在visual assist x中修改的,这个我找了半天才反应过来。
2.visaul assist x的open file in solution快捷键shift+alt+o没掉了,按这个快捷键打开的一个网页模板窗口。方式是配置一个新的快捷键。tools->options->environment->keyboard,打开面板后选中VAssistX.OpenFileInSolutionDialog。再将鼠标焦点切换到Press shortcut keys下面的输入框里面,按下shift+alt+o,就添加一个全局快捷键,点击assign按钮即可。如图,
3.vs的查找文件快捷键,shift+ctrl+f无效。这是因为安装了搜狗输入法造成了。搜狗输入法在更上层截断了这个快捷键,设置下搜狗输入法取消这个快捷键即可。如图,
4.调试uc时候游戏进程脱离vs。这个debug->attach to process即可。注意最上面的tansport可以筛选unrealscript,筛选后再用下面的refresh按钮刷新一下就出来了。
5.f12查找代码定义的问题并没有彻底解决。在uc文件中,只能用f12查找定义。对于f12失效的情况下,使用3在整个解决方案搜索,不过比较麻烦,但是能解决问题。在cpp文件中,就能够使用va的快捷键alt+g查找定义了。我居然忘记了这茬,以前从没接触过脚本编写,现在项目里面cpp和uc都有,所以混淆了。
6.uc断点无效,据说是nfringe的bug。
7.另外项目组的addin插件一直点击没反应,也不知道原因。不过,我在解决问题3之后,addin插件按钮点击后不会造成vs崩溃,但是还是无效的,也就是彻底没反应,我是服了。。。不过,今天前辈在我机子上面调试那个插件的代码,修复了bug,可以使用了,开心。。。
早上悲催的被发现昨晚上传代码没传全,差点请全组喝饮料了。。。
vector是一直在用的东西,太常用了,数组的替代品。大多数时候,都想不出理由不用vector,改用list,deque之类的。从功能来看,vector就是一个动态的数组而已。使用vector时候,唯一需要注意的是,当重新申请更大空间时候,是去了一块新内存,而不是直接在原地址增加,所有迭代器是都会失效的。当然还有其它操作造成某些迭代器失效。
下面来看看vector的部分定义。全部定义过于冗长了。
1 | template <class T, class Alloc = alloc> // 預設使用 alloc 為配置器 |
下面分几部分来讲述vector的定义。首先,模板参数一个是类型T作为vector的内含物类型,class Alloc = alloc则是使用默认的内存申请器。这一句typedef simple_alloc
然后,vector的迭代器类型其实就是原始的指针类型重定义了一下。以下这几句重定义,
1 | typedef T value_type; // (1) |
在list,deque,map,set等支持迭代器操作的容器定义中都会出现。其中,vector中的定义是最简单的。每个容器都有值类型,指针类型,迭代器类型,引用类型,以及大小类型,difference_type。知道这些后,写代码时候,可以避免一些错误,或者说代码更规范,就能自然而然的使用容易内置类型,比如vector
vector采用的数据结构很简单,就是一段线性内存。不过,有三个指针分别指向内存的头,已使用的尾部,尾。为了避免过多的重新分配内存,vector的容量永远比大小大,而且每次重新配置内存,大小翻倍。所以,vector的很多操作,比如,begin,end,front,back,size等的实现就很直观了,具体参看源码。下图很清楚的说明了这一情况。
接着要讲的是vector如何管理空间的。先看下面三个函数,
1 | vector(size_type n, const T value) { fill_initialize(n, value); } |
从源码中可以看出,最终都调用了函数uninitialized_fill_n初始化数据。这个stl的全局函数保证申请内存时候不会调用构造函数做额外的事情,也就是申请内存后,再用数据初始化。下面再看看其余几个操作。
1 | void push_back(const T x) { |
push_back在还有空间的情况下,也只需要调用construct函数构造数据,否则就需要调用insert_aux申请新的内存,再添加数据了。pop_back相对就简单很多。erase函数也涉及到一些移动内存块的操作,思路基本是先移动内存,然后把末尾多出来的数据析构掉(调用destroy函数)。
很久没更新博客了。额,一直想写这个来着。记得以前写过绘制三维标量场的文章,方法是找的渐变颜色纹理图片,然后用一维纹理坐标映射贴上去的。后面发现根本不需要去找图片,这种渐变色本来就是一种颜色空间的定义。请看hsv颜色模型,这种颜色模型类似于rgb,也是三个分量h(色调),s(饱和度),v(亮度)。h表示的绕圆锥的一周,也就是纯颜色的渐变,正是我们需要的东西。s表示从圆心到圆边缘的半径,0-1的范围,表示颜色纯度。v则是垂直的轴,表示亮度。如下图,我们要的就是绕圈一周的颜色渐变。
总体思路就是数值0-1,映射到hsv颜色空间的(0-360,1,255)。实现标量场的方法是,首先,将hsv的(0-360,1,255)转换为rgb,保存为一维数组。以该数组为数据,生成一维纹理,数值0-1作为纹理坐标,再贴图即可生成标量场。下面是用该方法生成标量场的相关代码。首先是构造一维纹理数组,然后用其生成1d纹理。
1 | int CTexture::BuildColorMapTexture() |
hsv2rgb的代码如下,从网上可以随便找个修改。
1 | PixelRGBA Hsv2Rgb(float H, float S, float V) |
剩下的事情就是使用BuildColorMapTexture生成1d纹理,然后将数值作为1d纹理坐标设置好就行了。下面是相关的效果图:
颜色空间转换生成纹理:
渐变纹理贴图:
不同的方法效果有区别,原因可能是贴图的数据更精细,而且同一范围比如0-0.3对应的颜色就可能不一致。马上要毕业了,工作内容是游戏开发,以后估计不会再更新读研期间类似的文章的了。
很久没骑这么远了。第一次骑三天都不知道是怎么坚持过来的。总的感觉,身体比去年好太多了。虽然脚累,但是呼吸完全不乱,完全不会心累。中午吃错东西了,一开始就来个闹肚子、过敏。我对某种食物强过敏,当时就在为自己生命安全思考了。本打算骑到附近的梅溪湖算了,但是吹了下风,觉得过敏消散了,就坚持走走看吧。
雷锋大道真的好远,从河西走完整个望城。经过去年的洗心禅寺,发现好近额。为啥去年我走这么点距离会觉得好累了。有些改变确实在不知不觉。不管是身,还是心。永远健康充满活力的身体,是追寻幸福的必要条件。
最后,附图一张。
排序的重要性就不用说了。stl里面的很多容器都是自序的,比如set和map,从begin到end就是一个顺序,毕竟最基本的概念就是一颗搜索树。另外,stack、queue、priority_queue的顺序也是内部实现决定的,不用多说了。list则不支持随机迭代器,内部也提供sort函数。所以,stl里面的sort泛型算法针对的是vector,deque,原生数组之类的数据。正常情况下,用的最多的也就是vector了。
插入排序和快速排序大家都知道是什么。但是,stl里面的sort并不仅仅是一个快速排序,而是一个复杂的组合体,有人把它叫做intro_sort。下面说一下introsort的几点要点。
1> 递归层次的限制,超过一定层次直接堆排序剩余数据。
2> 数据长度小于一定值,直接返回,不继续排序。
3>递归结束后,得到多段相互之间有序的数据(段内部无序),再进行一次优化的插入排序。
4>轴元素选取采用三点取中法,以避免分割不均。
sort的代码如下:
1 | template <class RandomAccessIterator> |
从代码中可以看到,sort内部调用的是introsort_loop排序得到多段互相之间有序的小段,再进行插入排序。其中,lg是计算递归层次,定义如下:
1 | // 找出 2^k <= n 的最大值k。例,n=7,得k=2,n=20,得k=4,n=8,得k=3。 |
因此,可以看出长度为64的数据,其递归层次不能超过16。如果超过,则进行堆排序。__introsort_loop的代码如下,是整个算法的关键。
1 | template <class RandomAccessIterator, class T, class Size> |
从代码里面可以看出,stl里面定义了个常数stl_threshold决定要不要继续循环,如果数据的长度比该值小,那么就不继续递归排序了,除非是调用了堆排,长度小于stl_threshold的段是没有经过排序的。当层次耗尽,depth_limit为0,直接调用partial_sort堆排序返回。另外的部分,就是采用三点取中法决定轴元素。还有这个代码写成了类似尾递归的形式,与教科书上不一致,较难阅读和理解。
__median的代码如下,容易理解。
1 | template <class T> |
__unguarded_partition与教科书上的partion实现不同,实现简单而且高效。其代码如下:
1 | template <class RandomAccessIterator, class T> |
现在回到sort函数中,最后的插入排序__final_insertion_sort。该函数再进行一次插入排序,由于小段之间都是有序的,因此该函数很快就能执行完。其代码如下:
1 | template <class RandomAccessIterator> |
该函数判断当前排序范围是不是小于等于于stl_threshold(16),如果是,直接进行优化的插入排序insertion_sort。否则,排序前面的16个元素,再调用unguarded_insertion_sort排序剩下的元素。unguarded_insertion_sort的相关定义代码如下:
1 | template <class RandomAccessIterator> |
__insertion_sort的相关代码如下:
1 | template <class RandomAccessIterator> |
从上面的代码可以看出,关于插入排序的代码比sort其它部分的代码都要多,可见stl对插入排序进行了特别的优化,以使得其在处理接近有序数据的时候,速度非常快。从代码中可以看到,这些函数最终都调用了__unguarded_linear_insert函数,该函数优化了插入排序的实现。该函数的思路是从后往前找是否存在比value小的元素,如果还存在,就往后移动数据一个位置。最终,得到的是value需要插入的位置。根据stl源码剖析作者的说法,该函数不需要判断越界,因此提高了速度。在大数据量的排序中,优势很大。毕竟,该函数会被sort频繁调用。
关于插入排序代码实现的具体思路,参考stl源码剖析一书。本文的代码也取自该书提供的源码。
这里只讲解稀疏矩阵表示的线性系统。最新版本的eigen已经支持稀疏矩阵,以及相关操作了。eigen相对其它库的最大优势,默认情况下,只需要eigen的头文件即可,不需要其它依赖库,除非用对应的加速解法(需要更快的库支持)。所以,eigen就是个牛逼的模板矩阵库。虽然在使用的时候需要套用不少模板参数,有点麻烦。
eigen里面定义稀疏矩阵,然后加入数据的一种方式如下:
1 | Eigen::SparseMatrix<float> smA(m_nRow, m_nColumn); |
代码非常简单,makeCompressed是可选的,具体原因不怎么清楚,参考官方文档。
知道定义了稀疏矩阵之后,就可以解稀疏线性系统Ax=b了。A作为稀疏系数矩阵输入。eigen里面有多种解线性系统的方法,官方文档有说明。其中,QR分解适合于任何大小的稀疏矩阵A,并且被推荐用来解最小二乘法问题,适用范围很广。下面的是QR分解求线性系统的代码。
1 | // 一个QR分解的实例 |
代码的思路是先定义一个QR分解的线性solver,然后调用compute分解。再传入b,再调用solve函数求解,再传出数据。逻辑很简单。值得注意的是,这里用了float来计算,是因为qr分解比较耗内存,如果矩阵过大或者非0值过多就会计算不过来。因此,用float来减少一半内存,不过精度比double下降了,具体看实际需要选择float还是double。至于QR分解的原理,请参考维基百科等。
如果使用迭代的办法解线性方程组,该怎么用eigen实现了。eigen里面也提供了迭代的solver,不过要求稀疏矩阵A是方阵。那么,把A转换为方阵即可。Ax=b的两边同时乘以A的转置,得到A’Ax=A’b(A’是A的转置),再用迭代solver求解。如果得到的答案误差较大,可以在两边乘以个(-1)。如下代码所示。
1 | //把A*X = b,转换为A'*A*X = A'*b,其中A'是A的转置,貌似计算结果不是很对,故没有使用该函数 |
迭代解法对应的头文件是#include “Eigen/IterativeLinearSolvers”。QR分解求线性系统的完整代码如下,相关定义很容易看懂。
1 | //适合于解决最小二乘法问题 |
QR分解对应的头文件是#include “Eigen/SparseQR”。</pre>
以前写过一篇C++调用MATLAB引擎计算特征向量,里面讲了如何配置环境等等。现在又有一个新的需求,求解线性系统。又想起了MATLAB这个工具,于是又写了个小类。这里要求解的是AX=B。其中,A是m*n的矩阵,X是n维行向量,B是m维列向量。MATLAB里面求解很简单X=A\B。
该类的头文件如下:
1 | //求解线性系统AX = B. |
源文件如下:
1 | #include "stdafx.h" |
该类里面就一个计算结果的函数。实现的过程也很简单,打开MATLAB引擎,创建了对应的矩阵和向量,计算表达式。注意,”X = A\B;”中必须加转义\。另外,MATLAB中的矩阵是列主序的,也就是按照列存储的。所以,从C++的矩阵变换到MATLAB矩阵需要重新设置值,而不是简单的拷贝内存。
如果,A是巨大的稀疏矩阵,那么就应该创建稀疏矩阵来计算了。测试代码如下:
1 | //测试线性系统 |
输出为:3.000000 -0.600000
前面说了,系数矩阵的事情,现在代码进行了更新,加入了稀疏矩阵,A可以是稀疏矩阵传入,B的话没有当做稀疏矩阵传入了。不过,在调用MATLAB代码时候还是得作为稀疏矩阵。具体代码如下,就不细说了。
1 | #pragma once |