名字长度怎么算

在游戏或者任何需要输入的软件系统里,必然少不了长度限制这一条,比如游戏里面的角色名,公会名,队伍名,那就少不了需要计算长度。

我们限制长度的目的就是在客户端那边看起来显示要正常,不能超框,不能刷屏。

最早,只在国内发行的时候,我们限制长度用简单的数字节的方法,一个汉字对应两个字节,其宽度也恰好约等于两个英文字符的宽度。再加上取名字符范围的限制,这个方法还是好用的。

现在,起名时不再限制字符范围,上面的方法实际已不再适用。举个例子,在泰语里 ·สองศูนย์พูนสวัสดิ์· 这个字符串的 String.length 是 18,但是如果我们按照英文字符那样数的话,基本上应该认为是 12。再加上 emoj 和 不同语言字符间距和宽度的不通,使用 String.length 或者数字节的方式都不能再较为正确的对应出字符串的实际显示长度了。

为了解决这个问题,我们的系统中的字符串长度计算,是把字符串真正的打印出来,然后数一下像素宽度,以英文字母 a 的宽度作为一标准字符宽度,通过计算得出大概的字符数。在打印之前,我们会通过字符串处理,去除名字开头和结束的控制字符,零宽字符和各种空格。

这个问题目前还存在这些问题:

  1. 服务器字体与客户端字体存在不一致的情况,导致两边计算的长度并不能完全一致,这里服务器仅限制长度在配置长度的合理范围内即可。
  2. 服务器字体不全,不能打印出所有的字符,不能打印的字符会被打印为替代字符,有固定的长度。目前是覆盖主要的测试测试区域的字符集。

相关关键代码如下:

    private static Font font;

    static {
        nameExcludePattern = Pattern.compile(NAME_EXCLUDE_REGEX);
        font = new Font("SansSerif", Font.PLAIN, 12);
    }

    public static int size(String name) {
        if (name == null || name.isEmpty()) return 0;
        BufferedImage img = new BufferedImage(100, 40, BufferedImage.TYPE_INT_ARGB);
        FontMetrics fm = img.getGraphics().getFontMetrics(font);
        return (int) Math.ceil(fm.stringWidth(name) / 7.0);
    }
   
    /**
     * 判断 char 是否是空格<br/>
     * 相关文档:<br/>
     * <a href="https://www.compart.com/en/unicode/category/Zs">unicode 空格定义</a><br/>
     * <a href="https://www.compart.com/en/unicode/U+3164#UNC_DB">u+3164</a>
     *
     * @param ch char
     * @return true 空格 false 不是空格
     */
    public static boolean isWhitespace(char ch) {
        return Character.isWhitespace(ch)
                || ch == '\u3164'
                || ch == '\u00A0'
                || ch == '\u2007'
                || ch == '\u202F';
    }

    public static String strip(String str) {
        return strip(str, ch -> ch <= 32 || isWhitespace(ch));
    }

    public static String strip(String str, final Predicate<Character> predicate) {
        Objects.requireNonNull(predicate);
        if (isEmpty(str)) {
            return str;
        }
        str = stripStart(str, predicate);
        return stripEnd(str, predicate);
    }

    public static String stripStart(final String str, final Predicate<Character> predicate) {
        Objects.requireNonNull(predicate);
        int strLen;
        if (str == null || (strLen = str.length()) == 0) {
            return str;
        }
        int start = 0;
        while (start != strLen && predicate.test(str.charAt(start))) {
            start++;
        }
        return str.substring(start);
    }

    public static String stripEnd(final String str, final Predicate<Character> predicate) {
        Objects.requireNonNull(predicate);
        int end;
        if (str == null || (end = str.length()) == 0) {
            return str;
        }

        while (end != 0 && predicate.test(str.charAt(end - 1))) {
            end--;
        }
        return str.substring(0, end);
    }

未采用方案,白名单方案:
制作一个包含所有需要显示内容的字体供服务器和客户端共同使用,使用 Font#canDisplay 或 Font#canDisplayUpTo 判断是否可显示此字符串,如不能显示,则返回错误。

参考文档:
找到的较好的字体入门中文文档:其一 其二
TrueType 文档
为什么没有一个字体可以包含所有的 unicode 字符
google notion 字体,试图包含所有 unicode 字符的字体集