游戏项目如何快速实现多语言版本(国际化)--Egret篇

本文主要针对Egret开发的项目做详细介绍,对其它引擎开发的项目亦有参考价值。

需求背景:
1、对整个游戏项目进行多语言版本(国际化)开发,把游戏内的文字由中文换成英文;
2、尽量不要修改业务代码,至少不需要手动修改,减少工作量。
3、不要遗漏,保证准确性;

需求分析:
1、游戏内文本主要包括系统文字文本和图片文本,图片很好处理,每种语言一个文件夹,美术修改图片的文字替换即可,程序根据语言字段调用对应文件夹;
2、关键点是系统文字的文本处理,其中文本分布在 ts文件和exml文件中,得想办法批量处理这2部分文字;

方法介绍:

  • 针对ts文件里面的文本: 写个脚本把项目中所有ts文件里面的汉字文本读出来汇总到一个Language.ts文件中,定义在枚举enum对象里面,并且同时批量修改源码把该汉字字符替换为枚举引用;
    注意脚本需要很好的维护Language.ts文件,项目源码增加了文本或删除了文本,要保证Language.ts文件的准确和干净;
    有朋友就要问了为什么用枚举,不能封装一个方法吗?答案是因为枚举可以有代码提示!这非常重要,让你的代码可读性大大提高。

  • 针对exml文件里面的文本: 同样先写个脚本把项目中exml文件里面的汉字文本读出来汇总到一个Language2.ts文件中,定义在一个Object对象里面;
    然后再修改类代码中的partAdded方法,判断如果是Label或Button,则把其text或label属性设置为对应的语言文本。
    可是,这一步代码写好之后实测会发现问题,那就是一部分文本替换不超过,原因是egret引擎默认并不是所有Label都会调用partAdded方法,只有设置了id的对象才会进调用。因此还需要修改一下引擎代码。在eui解析EXML时判断如果Label没有设置id,就自动为其分配1个id,这样就能保证所有Label会触发partAdded方法了

通过以上方法,无需修改具体的业务代码,完美的实现了需求,剩下的就是把汇总的2个汉字文本提交给翻译了。

下面附代码:


public partAdded(partName: string,instance: any): void {
    super.partAdded(partName,instance);
    if(window["_LangExml"]){ //定义在主项目
        let key;
        if(instance instanceof eui.Label){
            key = instance.text;
            if(window["_LangExml"].hasOwnProperty(key))
                instance.text = window["_LangExml"][key];
        }
        else if(instance instanceof eui.Button){
            key = instance.label;
            if(window["_LangExml"].hasOwnProperty(key))
                instance.label = window["_LangExml"][key];
        } 
    }
}
function changeEgretCodeForLange(){
    if(!window["_LangExml"]) return;
    console.error("启用了多语言版本,重写引擎 eui.sys.EXMLParser.prototype.addIds 方法");

    let fun = eui.sys.EXMLParser.prototype.addIds;
    eui.sys.EXMLParser.prototype.addIds = function(items){
        if (!items) {
            return;
        }
        var length = items.length;
        for (var i = 0; i < length; i++) {
            var node = items[i];
            if (node.nodeType === 1 && node.attributes["text"] && !node.attributes["id"]) {
                this.createIdForNode(node);
            }
        }
        fun.apply(this, [items]);
    }
}

脚本代码

/*
 * 从项目文件夹 提取汉字字符串,生成汉字字典 -----------------
 * 原理:遍历项目里面的汉字字符串,生成ts字典,再用enum枚举方式引用,可以在代码处直接查看枚举值,保证开发效率
*/

var fs = require('fs');
var path = require('path');

var filterFils = ["Language","Language2"];
var filterDirs = ["xx"];
var root_Url = "D:/xxxx/src";
var root_ExmlUrl = "D:/xxxx";

var outFilePath = "D:/xxxx/Language2.ts";
var outFilePath2 = "D:/xxxx/LanguageExml.ts";

var map = {};
var total = 0;

outLanguageTs(outFilePath);
outLanguageExml(outFilePath2);

//先把已经生成的读取出来,避免重复或覆盖
function outLanguageTs(outFilePath){

	let langContent = fs.readFileSync(outFilePath, { encoding: "utf8" });
	langContent = langContent.split("\n");
	if(langContent > 2){
		langContent.shift();
		langContent.pop();
		let str = ", //";
		for(var i=0; i<langContent.length; i++){
			if(i == langContent.length-1)
				str = " //";
			var item = (langContent[i].split(str)[0]).split(" = ");
			map[ item[1] ] = Number(item[0].substr("    str".length));

			if(i == langContent.length-1)
				total = map[ item[1] ];
		}
		console.log("已有文案:", map);
	}
	//return;

	fileDisplay(path.resolve(root_Url));
}

var timer;
//异步,不知道什么时候结束,用这个来判断
function reSetSave(reset){
	clearTimeout(timer);
	if(!reset){
		timer = setTimeout(()=>{
			let str = [];
			for(var key in map){
				console.log(key, map[key]);
				str.push( {v:"    str" + map[key] + " = " + key, key:key} );
			}

			let content = ["const enum Lang2 {"];
			for(var i=0; i<str.length - 1; i++){
				content.push(str[i].v + "," + " //" + str[i].key);
			}
			content.push(str[i].v + " //" + str[i].key); //最后1个不要逗号
			content.push("}")
			fs.writeFileSync(outFilePath, content.join("\n"), { encoding: "utf8" });

			console.log("保存文件:" + outFilePath);
		}, 1000);
	}
}

/**
 * 文件遍历方法
 * @param filePath 需要遍历的文件路径
 */
function fileDisplay(filePath){
	//根据文件路径读取文件,返回文件列表
	fs.readdir(filePath,function(err,files){
		if(err){
			console.warn(err)
		}else{
			//遍历读取到的文件列表
			files.forEach(function(filename){
				//获取当前文件的绝对路径
				var filedir = path.join(filePath,filename);
				//根据文件路径获取文件信息,返回一个fs.Stats对象
				fs.stat(filedir,function(eror,stats){
					if(eror){
						console.warn('获取文件stats失败');
					}else{
						var isFile = stats.isFile();//是文件
						var isDir = stats.isDirectory();//是文件夹
						if(isFile){
							//console.log(filename,filedir);
							let onlyName = filename.split(".")[0];
							if(filename.indexOf(".ts") != -1 && filterFils.indexOf(onlyName) == -1){
								changeDode(filedir);
							}
						}
						if(isDir){
							if(!filterDirs || filterDirs.indexOf(filename) == -1){
								fileDisplay(filedir);//递归,如果是文件夹,就继续遍历该文件夹下面的文件
							}
						}
					}
				})
			});
		}
	});
}

function changeDode(phppath){
	//console.log("执行文件:" + phppath);
	let phpContent = fs.readFileSync(phppath, { encoding: "utf8" });
	//console.log(phpContent);
	
	//['"][\xff-\uffff 0-9$]+['"]
	let arr = phpContent.match(/['"][\xff-\uffff 0-9$]+['"]/g);
	//console.log(arr);
	if(arr){
		let change, nstr, regExp0;
		for(var i=0; i<arr.length; i++){
			let str = arr[i];
			if(str.match(/['"][\d$  ]+['"]/g)) continue; //纯数字、空格、$串,忽略

			regExp0 = new RegExp("console\\.warn\\(" + str, 'gi');
			if(phpContent.match(regExp0)) continue; //console打印的内容,忽略

			regExp0 = new RegExp("console\\.log\\(" + str, 'gi');
			if(phpContent.match(regExp0)) continue; //console打印的内容,忽略
			
			regExp0 = new RegExp("console\\.error\\(" + str, 'gi');
			if(phpContent.match(regExp0)) continue; //console打印的内容,忽略

			//console.log(str);
			if(!map[str]){
				total++;
				map[str] = total;
				nstr = "Lang2.str" + total;
				reSetSave(); //有改变,触发延时保存
			}
			else{
				nstr = "Lang2.str" + map[str];
			}
			change = true;
			regExp0 = new RegExp(str, 'gi');
			phpContent = phpContent.replace(regExp0, nstr);
		}

		//console.log(phpContent);
		if(change){
			fs.writeFileSync(phppath, phpContent, { encoding: "utf8" });
			//console.log("执行完成!--" + phppath);
		}
	}
}

function Trim(str, is_global) {
	//console.log("==", str)
	str = String(str);
	var result;
	result = str.replace(/(^\s+)|(\s+$)/g,"");
	if(is_global && is_global.toLowerCase()=="g"){
		result = result.replace(/\s/g,"");
	}
	return result;
}
function lTrim(str) {
	str = String(str);
	var result = str.replace(/(^\s+)/g,"");
	return result;
}

//先把已经生成的读取出来,避免重复或覆盖
function outLanguageExml(outFilePath){
	let langContent = fs.readFileSync(outFilePath, { encoding: "utf8" });
	try{
		langContent = langContent.substr('window["_LangExml"] = '.length);
		langContent = langContent.replace(/\\n/g, "");
		map = JSON.parse(langContent);
	}catch(e){
	}
	console.log("已有文案:", map);

	fileDisplay2(path.resolve(root_ExmlUrl));
}

//异步,不知道什么时候结束,用这个来判断
function reSetSave2(reset){
	clearTimeout(timer);
	if(!reset){
		timer = setTimeout(()=>{
			let str = JSON.stringify(map);
			str = str.replace(/\\"/g, "");
			str = str.replace(/","/g, '",\n"');
			str = "{\n" + str.substr(1, str.length-2) + "\n}";
			fs.writeFileSync(outFilePath2, 'window["_LangExml"] = ' + str, { encoding: "utf8" });

			console.log("保存文件:" + outFilePath2);
		}, 1000);
	}
}

/**
 * 文件遍历方法
 * @param filePath 需要遍历的文件路径
 */
function fileDisplay2(filePath){
	//根据文件路径读取文件,返回文件列表
	fs.readdir(filePath,function(err,files){
		if(err){
			console.warn(err)
		}else{
			//遍历读取到的文件列表
			files.forEach(function(filename){
				//获取当前文件的绝对路径
				var filedir = path.join(filePath,filename);
				//根据文件路径获取文件信息,返回一个fs.Stats对象
				fs.stat(filedir,function(eror,stats){
					if(eror){
						console.warn('获取文件stats失败');
					}else{
						var isFile = stats.isFile();//是文件
						var isDir = stats.isDirectory();//是文件夹
						if(isFile){
							//console.log(filename,filedir);
							let onlyName = filename.split(".")[0];
							if(filename.indexOf(".exml") != -1 && filterFils.indexOf(onlyName) == -1){
								changeDode2(filedir);
							}
						}
						if(isDir){
							if(!filterDirs || filterDirs.indexOf(filename) == -1){
								fileDisplay2(filedir);//递归,如果是文件夹,就继续遍历该文件夹下面的文件
							}
						}
					}
				})
			});
		}
	});
}

function changeDode2(phppath){
	//console.log("执行文件:" + phppath);
	let phpContent = fs.readFileSync(phppath, { encoding: "utf8" });
	//console.log(phpContent);
	
	//['"][\xff-\uffff 0-9]+['"]
	let arr = phpContent.match(/['"][\xff-\uffff 0-9]+['"]/g);
	//console.log(arr);
	if(arr){
		let change, nstr, regExp0;
		for(var i=0; i<arr.length; i++){
			let str = arr[i];
			if(str.match(/['"][\d$  ]+['"]/g)) continue; //纯数字、空格、$串,忽略
			str = str.substr(1, str.length-2);

			//console.log(str);
			if(!map.hasOwnProperty(str)){
				map[str] = "";
				reSetSave2(); //有改变,触发延时保存
			}
		}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在 Egret 中,可以使用 RenderTexture 和 BlendMode 来实现渲染批次。 渲染批次是指将多个绘制操作合并为一个批次,从而减少绘制次数,提高游戏性能。 具体实现步骤如下: 1. 创建一个 RenderTexture 对象,将要渲染的显示对象添加到 RenderTexture 中。 2. 使用 BlendMode 设置渲染模式,将多个 RenderTexture 合并为一个批次。 示例代码如下: ``` // 创建一个 RenderTexture 对象 var renderTexture: egret.RenderTexture = new egret.RenderTexture(); renderTexture.drawToTexture(displayObject); // 设置 BlendMode renderTexture.blendMode = egret.BlendMode.ADD; ``` 在使用 RenderTexture 进行渲染时,可以将多个 RenderTexture 合并为一个批次,从而减少绘制次数,提高游戏性能。例如,可以将多个 RenderTexture 合并到一个 Bitmap 中进行渲染,代码如下: ``` // 创建一个 Bitmap 对象 var bitmap: egret.Bitmap = new egret.Bitmap(); bitmap.width = 800; bitmap.height = 600; // 创建多个 RenderTexture 对象 var renderTexture1: egret.RenderTexture = new egret.RenderTexture(); var renderTexture2: egret.RenderTexture = new egret.RenderTexture(); renderTexture1.drawToTexture(displayObject1); renderTexture2.drawToTexture(displayObject2); // 合并 RenderTexture bitmap.texture = new egret.RenderTexture(); bitmap.texture.drawToTexture(renderTexture1); bitmap.texture.drawToTexture(renderTexture2); // 设置 BlendMode bitmap.blendMode = egret.BlendMode.ADD; ``` 注意,使用渲染批次时需要注意渲染顺序和深度问题,以避免出现渲染错误的情况。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星星之Coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值