MeteorCat / Erlang进阶(二)

Created Sun, 14 Jul 2024 22:32:16 +0800 Modified Wed, 29 Oct 2025 23:24:53 +0800
1772 Words

Erlang进阶(二)

之前已经构筑好自己的游戏项目初期工程, 现在继续深入 Erlang 的进阶概念, 这次学习的新概念:

  • include: 头文件
  • spec: 强类型声明
  • record: 数据记录

这三个也算是 Erlang 所要了解的概念, 有利于构建出合理规范的项目结构.

include/头文件

如果之前学习过 C/C++ 语言就会知道很多会定义大量的 file.h/file.hpp 头文件, 而 Erlang 作为可以和 C/C++ 交互的语言也衔接上这些概念.

一般来说, Erlang 的头文件都是放置在项目根目录的 include 之中, 并且文件后缀以 .hrl 结尾.

这里假设定义全局配置头文件来编写些通用静态配置:

vim include/common.hrl

通用配置文件如下:

%%%-------------------------------------------------------------------
%%% @author MeteorCat
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%% 通用配置头文件
%%% @end
%%% Created : 14. 7月 2024 下午 10:51
%%%-------------------------------------------------------------------
-author("MeteorCat").


%%% 定义保护宏, 防止多次引入的重复定义
-ifndef(__ERLANG_COMMON__).
-define(__ERLANG_COMMON__, 1).


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%  项目相关的常量
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-define(PROJECT_NAME, "fight-game"). %% 服务端项目名


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%  配制文件目录信息, 根目录由外部传入配置
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-define(CONF_FILENAME_DIR, "config/"). %% 外部 yrl 配置文件名
-define(LOG_FILENAME, "logs/"). %% 日志文件目录


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%  重新定义对内部原子进行定义, 方便后续统一变动
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% 内部定义相关
-define(true, true). %% true  真/开
-define(false, false).  %% false 假/关
-define(null, null). % 空对象


% 其他返回相关
-define(ok, ok). %% ok
-define(error, error). %% error
-define(exit, exit). %% exit

% 时间相关
-define(microsecond, microsecond).


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%  自定义的头文件函数
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-define(IF(B, T, F), case (B) of ?true -> (T);?false -> (F) end). %% Erlang 三目运算模拟
-define(TIMESTAMP, fun() ->
  ErlangSystemTime = erlang:system_time(?microsecond),
  MegaSecs = ErlangSystemTime div 1000000000000,
  Secs = ErlangSystemTime div 1000000 - MegaSecs * 1000000,
  MegaSecs * 1000000 + Secs end). % 获取秒级时间戳

-endif.

之后就是引入并调用头内部的函数, 这里在命令行程序入口( fight.erl ) 配置测试:

%%%-------------------------------------------------------------------
%%% @author MeteorCat
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%% fight游戏服务端启动入口 -> fight:main
%%% @end
%%% Created : 14. 7月 2024 下午 07:31
%%%-------------------------------------------------------------------
-module(fight).
-author("MeteorCat").

-behaviour(application).
-include("../include/common.hrl"). % 引入项目通用头文件

%%--------------------------------------------------------------------
%% @private
%% @doc
%% 项目主要回调入口, -spec 代表强类型声明, 后续讲解的方法
%% @end
%%--------------------------------------------------------------------
-spec(start(StartType :: normal | {takeover, node()} | {failover, node()},
    StartArgs :: term()) ->
  {ok, pid()} |
  {ok, pid(), State :: term()} |
  {error, Reason :: term()}).
start(_StartType, _StartArgs) ->

  % 调用头文件宏展开的函数, 这里的 ?FILE 是系统定义的头文件内部值
  % 调用头定义的数据必须采用 `?XXX` 来使用
  io:fwrite("Fight Started ~w, Filename:~s~n", [?TIMESTAMP(), ?FILE]),

  % 暂停5秒之后调用确认时间误差是否正确
  receive after 5000 -> % 暂停5秒之后调用
    io:format("Sleep BY ~w~n", [?TIMESTAMP()])
  end,

  {?ok, erlang:self()}.

注意要使用头文件定义的数据必须先进入 include 之后通过 ?头文件常量, 可以看到上面头文件定义很多系统本来就有的原子量, 因为目前版本很多开发工具对于原子量( atom )扫描支持并不好, 所以为了约束规范原子量而将其作为头文件常量定义.

原子量定义在头文件定义为常量是扩展性和约束都很好的规范, 极其值得推荐的使用方法.

spec/强类型

虽然 Erlang 很少看到类型定义, 但是实际上内部是支持强制类型定义的, 并且如果你定义类型绝对不会变动是推荐直接采用强类型定义:

% 声明强类型方法, 主要就是
% 1. 声明强类型结构
% 2. 实现强类型方法
-spec calc(integer(), integer()) -> integer(). % 最后返回定义结果
calc(ArgLeft, ArgRight) -> % 最后总体实现的函数构建体
  ArgLeft * ArgRight.


% 这里定义声明传入参数类型, 花括号就是参数, 具体定义方式:  `type 参数名() :: { 传入对象A,传入对象B,传入对象C }.`
-type calc2_args() :: {integer(), integer(), integer()}.
-spec calc2(calc2_args()) -> integer(). % 最后返回定义结果
calc2(Args) -> % 最后总体实现的函数构建体
  [ArgA, ArgB, ArgC] = Args,
  ArgA * ArgB * ArgC.


% 入口方法
main() ->
  % 混合类型直接 badarith 异常
  % io:format("Calc ~w~n", [calc("test", 2)]),
  io:format("Calc1 ~w~n", [calc(4, 2)]),
  io:format("Calc2 ~w~n", [calc2([4, 2, 5])]),
  ok.

强类型实际上说的篇幅不多, 但是如果能够用强类型推断定义的函数就可以尽可能写上类型, 剩下就是需要用到时候可以查资料.

record/数据记录

Erlang 其实更像是元组(Tuple)结构; 通过 record 可以创建具有特定字段的含任何类型的数据结构(基本类型和复合类型); 当创建Record时可以省略某些字段而这些字段将使用默认值, 访问Record中的字段时可以直接使用字段名进行访问而无需记住字段在元组中的位置或索引.

record 应用访问特别广, 适合将数据映射到内存从而在内存上做数据操作, 常规来说都是单独头文件定义声明:

# 编写玩家信息 record
vim include/player_record.hrl

player_record.hrl 内容如下:

%%%-------------------------------------------------------------------
%%% @author MeteorCat
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 15. 7月 2024 上午 12:24
%%%-------------------------------------------------------------------
-author("MeteorCat").
-include("common.hrl"). % 引入通用, 因为做得 define 保护所以不用怕重复引入

%%% 定义保护宏, 防止多次引入的重复定义
-ifndef(__ERLANG_PLAYER_RECORD__).
-define(__ERLANG_PLAYER_RECORD__, 1).

%% 玩家实体
-record(player, {
  uid = 0, % 用户ID
  sid = 0, % 服务器ID
  socket = ?null, % 用户连接 Socket 对象
  pid = ?null, % 玩家所在进程id
  scene = ?null, % 场景进程


  nickname = ?null, % 玩家昵称
  money = 0, % 玩家充值货币金额, 单位:分
  lv = 1, % 玩家等级
  exp = 0, % 玩家经验值
  attr = ?null, % 角色属性(最终结算数值)

  net = ?null % 网络I/O对象
}).


-endif.

这里完美定义玩家实体的 record, 最后就是创建和增删改查:

%%% 确认 player_record.hrl 已经被引入
-include("../include/player_record.hrl"). % 引入项目通用头文件

%% 项目启动入口
main() ->
  % 构建出 record, 没有配置的数据会采用默认值
  Player = #player{
    uid = 1001,
    sid = 1,
    pid = erlang:self()
  },
  io:format("Player = ~p~n", [Player]),

  % 变动数据, 注意 Erlang 数据都具有不变性
  Player2 = Player#player{
    uid = 10002
  },
  io:format("Player = ~p~n", [Player2]),
  
  ok.

在游戏方向当中, record 基本上都会作为配置在程序做挂载内存数据, 然后通过定时写入到保存数据库当中(Backup, 备份者模式).