笔者最近刚做了个聊天系统,开发过程中踩了不少的坑,在此总结下经验教训,以便回顾参考,也希望他人看到后可以少走弯路。这里不全是贴代码,主要提供聊天系统的实现思路,以及需要注意的点。
聊天框图文混排
在【关于FLASH中图文混排聊天框的小结】一文中已经总结了几种图文混排的实现方式。对于不需要拉伸缩放的表情聊天框,可以直接用AS3的Textfield类自己实现两层结构的文本类,这种是最简单,笔者也是采用了这种做法。
聊天输入框如果没有特殊要求,不需要支持显示表情movieclip(后面简称mc),则一般采用AS3的TextInput组件足矣,即只能输入纯文本。(在我接触的很多款网游中,聊天输入框都是纯文本的)
既然输入框只支持纯文本,那怎么插入表情呢?这就需要实现图片与文字的互相转化了。实现原理并不难,简单来说,就是当玩家从表情面板选中表情时,自动将其转换成表情代码(格式自定),插入输入框中。在玩家发送消息后,进行文本解析,利用正则表达式将聊天消息里的表情代码解析替换成占位符(其实就是空格),然后在相应位置上将表情mc显示出来。
原理不难,难在实现细节。这里总结需注意的细节。
1、表情格式。表情格式不要选择过于复杂,过多字数的格式,越简单越好。笔者在初次实现时选择的格式是”[img]两位数字[/img]”,这样插入一个表情,实际等同于输入了13个字符,如果聊天限制字数以微博140个字作为标准,那只能插入10个表情,显然不合理。此外,简单的表情格式方便高频玩家直接手动输入表情代码,体验更好。
2、在玩家打字的过程中,有可能中途点击表情面板去插入表情,此时舞台焦点就不再为输入框(即输入框光标不再闪动,玩家插入表情后会发现不能继续打字),为了无损玩家聊天体验,需要在插入表情代码后,重设舞台焦点为输入框:
public function setFocus():void { //将舞台焦点设置为聊天输入框 Context.stage.focus = textField; //将聊天光标设置到文本末尾 textField.setSelection(textField.length, textField.length); }
3、半角空格≠全角空格,关于占位符的选择。在前文提过,我们需要利用正则表达式将表情替换成占位符,才能给表情movieclip预留足够位置显示。笔者建议占位符一定要使用全角空格(中文输入法中Shift+Space可切换半角/全角),因为行末刚好是全角空格时,文本会自动换行,半角空格则不会。如果使用半角空格作为占位符,就会出现一种情况,位于行末的表情代码刚好被替换成几个半角空格,即使该行的其他文本,加上几个空格的宽度已经超出了textField所设置的width值,该行文本仍然不会换行。这样就导致表情mc被添加到文本的外边。如下图所示,红框内为Textfield的width,由于使用半角空格作为占位符,有个表情mc华丽丽地跳脱出了文本框。因此,请使用全角空格做占位符!
4、正则表达式解析表情代码,这个可以说是整个图文混排文本最关键的代码了,其实也只寥寥几十行代码:
/** * 设置文本 外部添加内容请使用此方法 */ public function set htmlText(value:String):void { _htmlTxt = value; _textField.htmlText = value; checkImg(); addChildAt(_textField, 0); } public static const REG_IMG:RegExp = /#\d{2}/ig; /** * 查找图片标签
*/ private function checkImg():void { var content:String = _textField.text; var result:Array = []; var count:int; var objImg:Object; while(true) { //表情 objImg = REG_IMG.exec(content); if(objImg != null) { //#00 和下面的替换相差1个字符 objImg.index -= count * 1; result[result.length] = objImg; count++; }else break; } if(result.length > 0) { _htmlTxt = _htmlTxt.replace(REG_IMG, " ");//注:<font> </font> 用的全角空格 才能自动换行 _textField.htmlText = _htmlTxt; var obj:Object; for(var i:int=0; i< result.length; i++) { obj = result[i]; if(obj != null) { CaculatContent(obj.index,obj[0]); } } } } /** * 计算表情标签 */ private function CaculatContent(startIndex:int,value:String):void { var mcName:String = value.slice(1, value.length); var displayObj:Sprite = Reflection.createMovieClipInstance("Movie"+int(mcName)) as Sprite; if(displayObj == null) return; var rect:Rectangle = _textField.getCharBoundaries(startIndex); if(rect != null) { displayObj.x = rect.x; displayObj.y = rect.y; addChild(displayObj); } }
字符过滤
对消息敏感内容的过滤一般交由后台负责,前台负责过滤处理文本中的特殊字符,如html标签字符,转义字符等。
游戏聊天框里的一段文本可能有不同样式,不同颜色,一般人名还要手型显示,支持点击。因此一般使用htmlText方法设置文字,而不是text方法。htmlText是一个比text更为复杂的方法,它接受html标签。请看下面这段代码:
var str:String = "第一行<br>第二行\n第三行\r第四行:\t<u><a href='http://www.baidu.com'>这是个网站链接</a></u><img src='https://www.baidu.com/img/bdlogo.png'></img>";
var text:TextField = new TextField(); text.wordWrap = true; text.multiline = true; text.width = 200; text.height = 200; this.addChild(text); text.htmlText = str;
运行结果如下:
可以看到html标签以及转义字符,都实际起到了作用。如果不做过滤处理,就有可能被外挂制作者加以利用,在聊天包中的消息字段插入这些字符,用于刷屏以及散布非法链接,图片。
因此需要过滤掉转义字符,html标签,将它们变成单纯的显示文本。下面贴代码:
package { /** * 正则表达式过滤字符工具 * @author ShuchangLiu */ public class HtmlRegexpUtil { public function HtmlRegexpUtil() { } /** * 进行字符过滤 * @param input * @return */ public static function filter(input:String):String { input = replaceTag(input); input = replaceSlash(input); return input; } /** * 过滤转义字符 * @param input * @return */ public static function replaceSlash(input:String):String { input = input.replace(/\n/g, "\\n"); input = input.replace(/\r/g, "\\r"); input = input.replace(/\t/g, "\\t"); return input; } /** * * 基本功能:替换标记以正常显示 * <p> * * @param input * @return String */ public static function replaceTag(input:String):String { if (!hasSpecialChars(input)) { return input; } var filtered:String = ""; var c:String; for (var i:int = 0; i <= input.length - 1; i++) { c = input.charAt(i); switch (c) { case '<': filtered += "<"; break; case '>': filtered += ">"; break; case '"': filtered += """; break; case '&': filtered += "&"; break; default: filtered += c; } } return (filtered.toString()); } /** * * 基本功能:判断标记是否存在 * <p> * * @param input * @return boolean */ public static function hasSpecialChars(input:String):Boolean { var flag:Boolean = false; if ((input != null) && (input.length > 0)) { var c:String; for (var i:int = 0; i <= input.length - 1; i++) { c = input.charAt(i); switch (c) { case '>': flag = true; break; case '<': flag = true; break; case '"': flag = true; break; case '&': flag = true; break; } } } return flag; } } }
再看上面的例子,对文本进行过滤处理后的效果。
现在这些html标签,转义字符就失去相应的作用,只是单纯的文字了。在实际开发中,除了对这两点进行处理,还可以再进一步过滤掉网址。
小心掉入Textfield的坑!textWidth > width ?textHeight > height ?
相信大部分aser都很笃定,width >=textWidth, height >=textHeight是绝对成立的,难道文本实际宽度/高度还能超过我们设置的宽度/高度吗? 很遗憾,是的!在绝大部分情况下,Textfield实例的textWidth <=width,但不是100%成立,特定情况下,textWidth > width 。究其原因是其api实现并不完美,而导致这种情况的罪魁祸首,又是前文提及的半角空格!看下面的代码:
var str:String = "1 2"; var text:TextField = new TextField(); text.wordWrap = true; text.multiline = true; text.width = 50; this.addChild(text); text.htmlText = str; trace(text.width); //result:50 trace(text.textWidth); //result:192
笔者开发时因为不了解此点,掉进了坑。在MMORPG游戏中,如果在场景(附近)频道说话,除了在聊天框显示消息,通常场景人物还会弹出聊天冒泡。聊天冒泡皮肤则根据文字宽高动态调整宽度、高度。因此,我们可以通过下面代码简单调整皮肤宽度。
_skin = new BubbleRoundRectWithArrowSkin(); addChildAt(_skin.display, 0); _message = new TextField(); _message.multiline = true; _message.wordWrap = true; _message.width = TEXT_MAX_WIDTH; addChild(_message); var bubbleWidth:int; bubbleWidth = _message.textWidth+ _skin.borderWidth; bubbleHeight = _message.textHeight+ _skin.borderHeight; _skin.setSize(bubbleWidth, bubbleHeight);
注意,上面代码并非用width方法,而是用textWidth方法去获得实际的文本宽度。无论_message 文本内容是什么,在没有设置autoSize的情况下,_message.width的值是固定不变的,都为TEXT_MAX_WIDTH。但这里可能会出现问题,即实际文本宽度textWidth > width,导致聊天框背景会远远宽于文本。如下图所示:
使用TextField类显示文本,当文本行尾为半角空格时(即英文输入的空格),哪怕文本宽度已超出width值时,也不会自动换行。这样当玩家输入大量空格时,聊天框没有及时换行,导致聊天框被拉得过长,从而影响了场景显示。
这是Textfield的api实现得不好之处,因此最好还是自己判断宽高度是否超出了限制。
if(_message.textWidth> TEXT_MAX_WIDTH) bubbleWidth = TEXT_MAX_WIDTH + _skin.borderWidth; else bubbleWidth = _message.textWidth+ _skin.borderWidth; if(_message.textHeight> TEXT_MAX_HEIGHT) bubbleHeight = TEXT_MAX_HEIGHT + _skin.borderHeight; else bubbleHeight = _message.textHeight+ _skin.borderHeight;
这样的代码才能有效限制聊天框背景的宽高,避免某个玩家文字过长的冒泡,遮挡掉游戏场景的大部分显示。另外,还可以通过限制同屏场景聊天冒泡的最大数量,给聊天冒泡设置半透明等方法,来缓解冒泡遮挡场景的体验问题。