Android OpenGL使用GLSurfaceView预览视频
Android OpenGL使用GLSurfaceView预览视频
第一章 相关知识介绍
在介绍具体的功能之前,先对一些主要的类和方法进行一些介绍,这样可以更好的理解整个程序
1.1 GLSurfaceView
在谷歌的官方文档中是这样解释GLSurfaceView的:
An implementation of SurfaceView that uses the dedicated surface for displaying OpenGL rendering.
大意是GLSurfaceView是一个继承了SurfaceView类,它是专门用来显示OpenGL的渲染。通俗的来说,GLSurfaceView可以用来显示视频、图像和3D模型等视图,在接下来的章节中主要是使用它来显示Camera视频数据,大家可能会有一些问题,SurfaceView也可用来预览Camera,那么这两者有什么区别吗?GLSurfaceView能够真正做到让Camera的数据和显示分离,我们就可以在此基础上对视频数据做一些处理,例如美图,增加特效等。
1.2 GLSurfaceView.Renderer
如果说GLSurfaceView是画布,那么仅仅有一张白纸是没用的,我们还需要一支画笔,Renderer的功能就是这里说的画笔。Renderer是一个接口,主要包含3个抽象的函数:onSurfaceCreated
、onDrawFrame
、onSurfaceChanged
,从名字就可以看出,分别是在SurfaceView创建、视图大小发生改变和绘制图形时调用。
1.3 Camera
从Android 5.0开始(API Level 21),可以完全控制安卓设别相机的新API Camera2(android.hardware.Camera2)
被引进来了。虽然新的Camera2不管在功能上还是友好度上都强于旧的Camera,但是我们这里还是使用的旧的Camera,由于新的Camera2暂时还没有找到可以获取视频帧的接口,因为后面肯能会对Canmera视频帧做一些处理,所以这里暂时还是使用旧的Camera。
第二章 开始绘制
2.1 CameraGLSurfaceView
public class CameraGLSurfaceView extends GLSurfaceView implements Renderer, SurfaceTexture.OnFrameAvailableListener {private Context mContext;private SurfaceTexture mSurface;private int mTextureID = -1;private DirectDrawer mDirectDrawer;public CameraGLSurfaceView(Context context, AttributeSet attrs) {super(context, attrs);mContext = context;// 设置OpenGl ES的版本为2.0setEGLContextClientVersion(2);// 设置与当前GLSurfaceView绑定的RenderersetRenderer(this);// 设置渲染的模式setRenderMode(RENDERMODE_WHEN_DIRTY);}@Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {// TODO Auto-generated method stubLOG.logI("onSurfaceCreated...");mTextureID = GlUtil.createTextureID();mSurface = new SurfaceTexture(mTextureID);mSurface.setOnFrameAvailableListener(this);mDirectDrawer = new DirectDrawer(mTextureID);CameraCapture.get().openBackCamera();}@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {// TODO Auto-generated method stubLOG.logI("onSurfaceChanged...");// 设置OpenGL场景的大小,(0,0)表示窗口内部视口的左下角,(w,h)指定了视口的大小GLES20.glViewport(0, 0, width, height);if (!CameraCapture.get().isPreviewing()) {CameraCapture.get().doStartPreview(mSurface);}}@Overridepublic void onDrawFrame(GL10 gl) {// TODO Auto-generated method stubLOG.logI("onDrawFrame...");// 设置白色为清屏GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);// 清除屏幕和深度缓存GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);// 更新纹理mSurface.updateTexImage();mDirectDrawer.draw();}@Overridepublic void onPause() {// TODO Auto-generated method stubsuper.onPause();CameraCapture.get().doStopCamera();}@Overridepublic void onFrameAvailable(SurfaceTexture surfaceTexture) {// TODO Auto-generated method stubLOG.logI("onFrameAvailable...");this.requestRender();}}
这个类主要做了以下几件事情:
- 实现Renderer这个接口,并且实现GLSurfaceView的初始化。在
CameraGLSurfaceView
的构造函数中设置了GLSurfaceView的版本:setEGLContextClientVersion(2)
,如果没有这个设置,GLSurfaceView是什么也绘制不出来的,因为Android支持OpenGL ES1.1、2.0以及3.+等版本,而且版本间的差别很大,不声明版本号,GLSurfaceView是不知道使用哪个版本进行渲染;设置Renderer与当前的View绑定,然后再设置渲染的模式为RENDERMODE_WHEN_DIRTY
。渲染模式的设置也很关键,渲染模式有两种:RENDERMODE_WHEN_DIRTY
和RENDERMODE_CONTINUOUSLY
。DIRYT的含义是只有当被通知的时候才会去渲染视图,而CONTINUOUSLY的含义是视频会一直连续的渲染。 - 在
onSurfaceCreated()
函数中,创建一个渲染的纹理,这个纹理就是用来显示Camera的图像,所以需要新创建的SurfaceTexture
绑定在一起,而SurfaceTexture
是作为渲染的载体,另一方面需要和DirectDrawer
绑定在一起,DirectDrawer
是用来绘制图像的,下面会具体介绍。最后是初始化Camera。 - 因为在初始化的时候这是了渲染的模式为
RENDERMODE_WHEN_DIRTY
,所以我们就通知GLSurfaceView什么时候需要渲染图像,而接口SurfaceTexture.OnFrameAvailableListener
完成这项工作,函数onFrameAvailable()
在有新数据到来时,会被调用,在其中调用requestRender(),就可以完成新数据的渲染。 - 在
onSurfaceChanged()
函数中,设置了OpenGL窗口的大小,(0,0)表示窗口内部视口的左下角,(w,h)指定了视口的大小;打开Camera的预览。 - 最后,在
onDrawFrame()
函数中绘制更新的纹理。
2.2 DirectDrawer
这个类非常重要,负责将SurfaceTexture(纹理的句柄)内容绘制到屏幕上。
public class DirectDrawer {private FloatBuffer vertexBuffer, mTextureCoordsBuffer;private ShortBuffer drawListBuffer;private final int mProgram;private int mPositionHandle;private int mTextureCoordHandle;private int mMVPMatrixHandle;private short drawOrder[] = {0, 2, 1, 0, 3, 2}; // order to draw vertices// number of coordinates per vertex in this arrayprivate final int COORDS_PER_VERTEX = 2;private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertexprivate float mVertices[] = new float[8];private float mTextureCoords[] = new float[8];private float mTextHeightRatio = 0.1f;private int texture;public float[] mMVP = new float[16];public void resetMatrix() {mat4f_LoadOrtho(-1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, mMVP);}public DirectDrawer(int texture) {String vertextShader = TextResourceReader.readTextFileFromResource(MyApplication.getContext(), R.raw.video_vertex_shader);String fragmentShader = TextResourceReader.readTextFileFromResource(MyApplication.getContext(), R.raw.video_normal_fragment_shader);mProgram = GlUtil.createProgram(vertextShader, fragmentShader);if (mProgram == 0) {throw new RuntimeException("Unable to create program");}//get handle to vertex shader's vPosition membermPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");GlUtil.checkLocation(mPositionHandle, "vPosition");mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");GlUtil.checkLocation(mTextureCoordHandle, "inputTextureCoordinate");mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");GlUtil.checkLocation(mMVPMatrixHandle, "uMVPMatrix");this.texture = texture;// initialize vertex byte buffer for shape coordinatesupdateVertices();setTexCoords();// initialize byte buffer for the draw listByteBuffer dlb = ByteBuffer.allocateDirect(drawOrder.length * 2);dlb.order(ByteOrder.nativeOrder());drawListBuffer = dlb.asShortBuffer();drawListBuffer.put(drawOrder);drawListBuffer.position(0);mat4f_LoadOrtho(-1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, mMVP);}public void draw() {GLES20.glUseProgram(mProgram);GLES20.glActiveTexture(GLES20.GL_TEXTURE0);GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture);// get handle to vertex shader's vPosition member// Enable a handle to the triangle verticesGLES20.glEnableVertexAttribArray(mPositionHandle);// Prepare the <insert shape here> coordinate dataGLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);GLES20.glEnableVertexAttribArray(mTextureCoordHandle);GLES20.glVertexAttribPointer(mTextureCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, mTextureCoordsBuffer);// Apply the projection and view transformationGLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVP, 0);GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);// Disable vertex arrayGLES20.glDisableVertexAttribArray(mPositionHandle);GLES20.glDisableVertexAttribArray(mTextureCoordHandle);}public static void mat4f_LoadOrtho(float left, float right, float bottom, float top, float near, float far, float[] mout) {float r_l = right - left;float t_b = top - bottom;float f_n = far - near;float tx = -(right + left) / (right - left);float ty = -(top + bottom) / (top - bottom);float tz = -(far + near) / (far - near);mout[0] = 2.0f / r_l;mout[1] = 0.0f;mout[2] = 0.0f;mout[3] = 0.0f;mout[4] = 0.0f;mout[5] = 2.0f / t_b;mout[6] = 0.0f;mout[7] = 0.0f;mout[8] = 0.0f;mout[9] = 0.0f;mout[10] = -2.0f / f_n;mout[11] = 0.0f;mout[12] = tx;mout[13] = ty;mout[14] = tz;mout[15] = 1.0f;}public void updateVertices() {final float w = 1.0f;final float h = 1.0f;mVertices[0] = -w;mVertices[1] = h;mVertices[2] = -w;mVertices[3] = -h;mVertices[4] = w;mVertices[5] = -h;mVertices[6] = w;mVertices[7] = h;vertexBuffer = ByteBuffer.allocateDirect(mVertices.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(mVertices);vertexBuffer.position(0);}public void setTexCoords() {mTextureCoords[0] = 0;mTextureCoords[1] = 1 - mTextHeightRatio;mTextureCoords[2] = 1;mTextureCoords[3] = 1 - mTextHeightRatio;mTextureCoords[4] = 1;mTextureCoords[5] = 0 + mTextHeightRatio;mTextureCoords[6] = 0;mTextureCoords[7] = 0 + mTextHeightRatio;mTextureCoordsBuffer = ByteBuffer.allocateDirect(mTextureCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(mTextureCoords);mTextureCoordsBuffer.position(0);}
}
这个类的主要功能就是绘制图像。
(1) 定义Vertex Shader(顶点着色器,用来绘制图形的形状)、Fragment Shader(片段着色器,用来绘制图形的颜色或者纹理)和Program(OpenGL ES对象,包含了用来绘制一个或者多个形状的shader),然后接下来都是围绕着这三个变量,最后通过调用OpenGL方法进行绘制。具体的过程可以参考前面的博客 使用OpenGL ES显示图形
(2) 既然我们需要预览Camera的视频数据,那么我们可以知道现实的区域的形状大部分都是四边形,但是在OpenGL中只有提供了绘制三角形的方法,我们就需要将两个三角形拼接成一个正方形,所以需要定义一个大小为8的数组,如下面代码所示:
static float squareCoords[] = { -1.0f, 1.0f, // 左上点-1.0f, -1.0f, // 左下点1.0f, -1.0f, // 右下点1.0f, 1.0f, // 有上点};
- 我们就有了一个四边形的4个点的数据了。但是,OpenGL并不是对数组的数据直接进行操作的,而是在直接内存中,即操作的数据需要保存到NIO里面的Buffer对象中。而我们上面生命的float[]对象保存在数组中,因此我们需要将float[]对象转换为Java.nio.Buffer对象,代码如下:
public void updateVertices() {final float w = 1.0f;final float h = 1.0f;mVertices[0] = -w;mVertices[1] = h;mVertices[2] = -w;mVertices[3] = -h;mVertices[4] = w;mVertices[5] = -h;mVertices[6] = w;mVertices[7] = h;vertexBuffer = ByteBuffer.allocateDirect(mVertices.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(mVertices);vertexBuffer.position(0);}
注意,ByteBuffer和FloatBuffer以及IntBuffer都是继承自抽象类java.nio.Buffer。
另外,OpenGL在底层的实现是C语言,与Java默认的数据存储字节顺序可能不同,即大端小端问题。因此,为了保险起见,在将数据传递给OpenGL之前,我们需要指明使用本机的存储顺序。
此时,我们顺利地将float[]转为了FloatBuffer,后面绘制三角形的时候,直接通过成员变量mTriangleBuffer即可。
(3) 最后就是将准备好的数据绘制到屏幕上,OpenGL 提供了两个绘制的方法glDrawArrays(int mode, int first, int count)
和glDrawElements(int mode,int count, int type, Buffer indices)
两个方法,在这里我们使用的第二种绘制的方法,关于mode有几种模式供我们选择:
GL_POINTS
:绘制独立的点到屏幕GL_LINE_STRIP
:连续的连线,第n个顶点与第n-1个顶点绘制一条直线GL_LINE_LOOP
:与上一个相同,但是需要首尾相联接GL_LINES
:形成对的独立的线段GL_TRIANGLE_STRIP
:绘制一系列的三角形,先是顶点v0,v1,v2,然后是v2,v1,v3(注意规律),然后v2,v3,v4等。该规律确保所有的三角形都以相同的方向绘制GL_TRIANGLE_FAN
和GL_TRANGLE_STRIP
类似,但其县绘制v0,v1,v2,再是v0,v2,v3,然后v0,v3,v4等。
(4) 需要注意的是,在这个类中,定义了mMVP这个数组,这个数组的功能是对视频帧数据进行转换的,例如旋转图像等。
总结
到此为止,使用GLSurfaceView预览Camera的介绍就完了,这篇文章,仅仅介绍了CameraGLSurfaceView
和DirectDrawer
这两个类,但是如何对Camera进行操作的并没有介绍,这不是本文的重点,所以就省略了。接下来还会介绍一些有关GLSurfaceView的文章。
Android OpenGL渲染双视频
下载代码
特别声明文章转载自:https://blog.csdn.net/a296777513/article/details/63685658
相关文章:

【Android 基础】Animation 动画介绍和实现
转载自:http://www.cnblogs.com/yc-755909659/p/4290114.html1.Animation 动画类型Android的animation由四种类型组成:XML中alph渐变透明度动画效果scale渐变尺寸伸缩动画效果translate画面转换位置移动动画效果rotate画面转移旋转动画效果JavaCode中Alp…

【Codeforces】1111B - Average Superhero Gang Power
http://codeforces.com/problemset/problem/1111/B n 表示要输入的数据的个数 k 最每一个数据最多可以进行多少次操作 m 一共可以进行多少次操作 一次操作:删除这个数,或者给这个数加1 如果n为1的话,那么只要找出m和k的最小值加到那个数…
刷前端面经笔记(七)
1.描述一下渐进增强和优雅降级 优雅降级(graceful degradation):一开始就构建站点的完整功能,然后针对浏览器测试和修复。渐进增强(progressive enhancement):一开始只构建站点的最少特性,然后不断针对各浏览器追加功能。 2.为什么…

AR资料与连接梳理
AR引擎相关技术 ------------------------------ ARcore:https://developers.google.cn/ar/discover/ ARkit:https://developer.apple.com/arkit/ 以上重点关注,比较新有一些新的功能大家可以自行体验。 ARToolkithttp://www.artoolkit.orght…

Queues 队列
1. Definiation What is a queue? A queue is a list. With a queue, inseration is done at one end (known as rear) whereas deletion is performed at the other end (known as front). 2. Operations 指针对列 无法自定义队长 // array queue #include<iostream> u…

【HDU】1005 Number Sequence (有点可爱)
http://acm.hdu.edu.cn/showproblem.php?pid1005 A number sequence is defined as follows: f(1) 1, f(2) 1, f(n) (A * f(n - 1) B * f(n - 2)) mod 7. Given A, B, and n, you are to calculate the value of f(n). 直接递归求解f(n)的话,会MLE 在计算…
CNCF案例研究:奇虎360
公司:奇虎360地点:中国北京行业:计算机软件 挑战 中国软件巨头奇虎360科技的搜索部门,so.com是中国第二大搜索引擎,市场份额超过35%。该公司一直在使用传统的手动操作来部署环境,随着项目数量的…

C#代码实现对Windows凭据的管理
今天有个任务,那就是使用C#代码实现对windows凭据管理的操作。例如:向windows凭据管理中添加凭据、删除凭据以及查询凭据等功能。于是乎,就开始在网上查找。经过漫长的查询路,终于在一片英文博客中找到了相关代码。经过实验&#…

Android:JNI 与 NDK到底是什么
前言 在Android开发中,使用 NDK开发的需求正逐渐增大但很多人却搞不懂 JNI 与 NDK 到底是怎么回事今天,我将先介绍JNI 与 NDK & 之间的区别,手把手进行 NDK的使用教学,希望你们会喜欢 目录 1. JNI介绍 1.1 简介 定义&…
【ACM】LightOJ - 1008 Fibsieve`s Fantabulous Birthday (找规律,找...)
https://vjudge.net/problem/LightOJ-1008 题目很好理解,第一行表示测试样例的个数,接下来输入一个大于等于1的数,按照格式输出这个数的坐标 蓝色的是 奇数的平方; 红色的是 偶数的平方; 黄色的是对角线:…
Computed property XXX was assigned to but it has no setter
报错视图: 原因: 组件中v-model“XXX”,而XXX是vuex state中的某个变量vuex中是单项流,v-model是vue中的双向绑定,但是在computed中只通过get获取参数值,没有set无法改变参数值解决方法: 1.在co…

OpenGL 矩阵变换
origin refer :http://www.songho.ca/opengl/gl_transform.html#modelviewOpenGL 矩阵变换Related Topics: OpenGL Pipeline, OpenGL Projection Matrix, OpenGL Matrix Class Download: matrixModelView.zip, matrixProjection.zipOverviewOpenGL Transform MatrixExample: GL…

2016.8.11 DataTable合并及排除重复方法
合并: DataTable prosxxx; DataTable pstaryyy; //将两张DataTable合成一张 foreach (DataRow dr in pstar.Rows) { pros.ImportRow(dr); } DataTable设置主键,并判断重复 DataTable allpros xxx; 单列设为主键: //设置第某列为主键 allpros.…

【ACM】LightOJ - 1010 Knights in Chessboard(不是搜索...)
https://vjudge.net/problem/LightOJ-1010 给定一个mn的棋盘,你想把棋子放在哪里。你必须找到棋盘上最多可以放置的骑士数量,这样就不会有两个骑士互相攻击。不熟悉棋手的注意,棋手可以在棋盘上攻击8个位置,如下图所示。 不论输入…

webpack-dev-server 和webapck --watch的区别
webpack-dev-server 和webapck --watch 都可以监测到代码变化 , 区别是:webpack-der-server 监测到代码变化后,浏览器可以看到及时更新的效果,但是并没有自动打包修改的代码; webpack --watch 在监测到代码变化后自动打…
Android 应用进行性能分析/APP/系统性能分析
如何对 Android 应用进行性能分析记录一下自己在使用DDMS的过程:开启AS,打开并运行项目 找到TOOL/选择Android Device Monitor一款 App 流畅与否安装在自己的真机里,玩几天就能有个大概的感性认识。不过通过专业的分析工具可以使我们更好的分…

公钥与私钥,HTTPS详解
1.公钥与私钥原理1)鲍勃有两把钥匙,一把是公钥,另一把是私钥2)鲍勃把公钥送给他的朋友们----帕蒂、道格、苏珊----每人一把。3)苏珊要给鲍勃写一封保密的信。她写完后用鲍勃的公钥加密,就可以达到保密的效果。4)鲍勃收信后,用私钥…

【ACM】杭电OJ 4704 Sum (隔板原理+组合数求和公式+费马小定理+快速幂)
http://acm.hdu.edu.cn/showproblem.php?pid4704 1.隔板原理 1~N有N个元素,每个元素代表一个1.分成K个数,即在(N-1)个空挡里放置(K-1)块隔板 即求组合数C(N-1,0)C(N-1,1)...C(N-1,N-1) 2.组合数求和公式 C(n,0)C(…
Vue 中 CSS 动画原理
下面这段代码,是点击按钮实现hello world显示与隐藏 <div id"root"><div v-if"show">hello world</div><button click"handleClick">按钮</button> </div> let vm new Vue({el: #root,data: {s…

【ACM】UVA - 340 Master-Mind Hints(一定要好好学英语...)
https://vjudge.net/problem/UVA-340 N 表示 密码的个数 第一行是正确的密码 下面的行直到N个0之前,都是猜测的序列,输出的括号(A,B) A表示对应位置与密码相符合的个数,B表示出现在序列中的数字但是位…
SLAM的前世今生
SLAM的前世 从研究生开始切入到视觉SLAM领域,应用背景为AR中的视觉导航与定位。 定位、定向、测速、授时是人们惆怅千年都未能完全解决的问题,最早的时候,古人只能靠夜观天象和司南来做简单的定向。直至元代,出于对定位的需求&a…

No resource found that matches the given name '@style/Theme.AppCompat.Light'
为什么80%的码农都做不了架构师?>>> Android导入项目时出现此问题的解决办法: 1.查看是否存在此目录(D:\android-sdk\extras\android\support\v7\appcompat),若没有此目录,在项目右键Android T…
极限编程 (Extreme Programming) - 迭代计划 (Iterative Planning)
(Source: XP - Iteration Planning) 在每次迭代开始时调用迭代计划会议,以生成该迭代的编程任务计划。每次迭代为1到3周。 客户从发布计划中按照对客户最有价值的顺序选择用户故事进行此次迭代。还选择了要修复的失败验收测试。客户选择的用户故事的估计总计达到上次…
VINS-mono详细解读与实现
VINS-mono详细解读 VINS-mono详细解读 前言 Vins-mono是香港科技大学开源的一个VIO算法,https://github.com/HKUST-Aerial-Robotics/VINS-Mono,是用紧耦合方法实现的,通过单目IMU恢复出尺度,效果非常棒。 感谢他们开源&#x…

mysql+mycat搭建稳定高可用集群,负载均衡,主备复制,读写分离
数据库性能优化普遍采用集群方式,oracle集群软硬件投入昂贵,今天花了一天时间搭建基于mysql的集群环境。 主要思路 简单说,实现mysql主备复制-->利用mycat实现负载均衡。 比较了常用的读写分离方式,推荐mycat,社区活…

【UVA/Codeforces】1584 Circular Sequence / 792B Counting-out Rhyme(就是一个圈儿...)
https://vjudge.net/problem/UVA-1584 1584 Circular Sequence 输入一个字符串,可以以字符串中任意一个字母作为起始,输出字典序最小的那个字符串 两种方法,一种是设两个标记 【样例输入】CGAGTCAGCT 【样例输出】AGCTCGAGTC 一开始 an…

一个free异常引发的异常
有同事反馈说自己的线程不工作,查看堆栈发现其打印如下: #0 0x00007f291f72e7be in __lll_lock_wait_private () from /lib64/libc.so.6 #1 0x00007f291f6c2e4e in _L_lock_9925 () from /lib64/libc.so.6 #2 0x00007f291f6c1101 in free () from /li…
欧拉角和旋转矩阵相互转换
目录 1.参考资料 2.变换矩阵/F/H的svd分解或者旋转矩阵、平移矩阵求解 3. 欧拉角和旋转矩阵可同样表示刚体在三维空间的旋转,下面分享这两者互相转换的方法和核心代码 1.参考资料 2.变换矩阵/F/H的svd分解或者旋转矩阵、平移矩阵求解 欧拉角转旋转矩阵 欧拉角…

【Codeforces】 2A - Winner (map)
http://codeforces.com/problemset/problem/2/A So, if two or more players have the maximum number of points (say, it equals to m) at the end of the game, than wins the one of them who scored at least m points first. 所以只有一个只有一个map不行,…

[译]Godot系列教程一 - 场景与节点
场景(Scene)与节点(Node) 简介 先设想有那么一瞬间你自己不再是一名游戏开发者了,而是一名大厨! 你的装备换成了一套大厨的制服。不要考虑制作游戏的事情,你现在的职责是为你的顾客创建新的可口的食谱。 那么,大厨是怎样创建食谱的…