`

基于Java的2D mmorpg开源引擎Threerings系列之四(实战聊天室)

阅读更多

通过前面几篇文章的介绍相信大家对Threerings这个框架已经有了初步的了解了,不过理论再多始终还是理论,只有通过不断实践才能真正掌握,今天我们就来应用这个框架来创建一个简单的聊天室程序,下图是这个聊天室应用的客户端界面,简单的包括了聊天记录区域,用户列表,聊天室编号列表和用户输入等。整个应用分为服务端和客户端两个部分。源代码可以在这里下载聊天应用程序helloworld

 


 


在前面几篇文章中,我们已经介绍过DObject以及框架内部的一些通讯机制。今天我们就要利用这些DObject来帮我们发送聊天消息和用户信息给其他用户。在此之外,我们还会用到经典的MVC模型来创建我们的应用程序。听起来好像有点复杂,不过好在我们有强大框架的支持,在narya库中,在presents框架之上,Threerings还提供给我们一个叫做crowd的框架,今天我们就要利用这个MVC框架来创建我们的聊天室应用程序。

 

 在正式编码之前,首先来搭建我们的环境。

 

如果你已经按照第一篇文章中介绍的那样,签出了源码库到本地的话,请运行distall来build所有的项目。build好之后在threerings\gardens目录下会生成一个dist目录,所有的项目都会被编译成jar包存放在dist\lib下。接下来我们创建一个聊天室项目的根目录,姑且就叫chat好了,然后把threerings\gardens\dist\lib复制到chat\lib下,这样我们就有了项目所需的依赖库文件。如果你没有签出到本地也没关系,可以直接在这里下载part1part2。再接下来我们需要为整个项目配置一个ant的build文件,为了方便起见,我们直接从threerings\gardens\projects\games\lib目录下把game-incl.xml复制到chat目录下并改名为build.xml。最后我们在chat目录下再创建两个java源文件目录src\java\net\tutorial\client,src\java\net\tutorial\server和src\java\net\tutorial\data。最终的项目工作目录看起来应该是这样子的:

 

chat/
      build.xml
      lib/
            activation.jar
            ant.jar
            aopalliance.jar
            ......
            ......
      src/
            java/
                  net/
                  tutorial/
                        client/
                        data/
                        server/

 

 现在我们需要修改下build.xml文件来适应我们的项目。首先加入我们的项目信息,项目名称就叫helloworld好了,然后再稍稍调整下目录结构,整个文件的开头部分看起来就变成这个样子:

 

<project name="helloworld" default="compile" basedir=".">

  <property name="app.name" value="helloworld"/>
  <property name="src.dir" value="src/java"/>
  <property name="deploy.dir" value="dist"/>

  <!-- declare our classpath -->
  <path id="classpath">
    <fileset dir="lib" includes="**/*.jar"/>
    <pathelement location="${deploy.dir}/classes"/>
  </path>

 

接下来再修改下prepare任务,修改后的prepare任务看起来就是这样子的:

 

  <!-- prepares the application directories -->
  <target name="prepare">
    <mkdir dir="${deploy.dir}"/>
    <mkdir dir="${deploy.dir}/classes"/>
  </target>

 

 最后再加入这样两个任务:

 

  <!-- a target for running a game client -->
  <target name="client">
    <java classname="net.tutorial.client.HelloWorldChat" fork="true">
      <classpath refid="classpath"/>
    </java>
  </target>

  <!-- a target for running the game server -->
  <target name="server">
    <java classname="net.tutorial.server.HelloWorldServer" fork="true">
      <classpath refid="classpath"/>
    </java>
  </target>

 

 现在我们的环境已经都配置好了,下面这几个ant任务已经都可以使用了

 

  • dist:编译并打包
  • server:开启服务器
  • client:开启一个客户端实例
  • gendobj:生成DObject (后面会讲到)

 在正式编码之前我们先把接下去要做的事情简单列个提纲。

 

1. 创建服务端

  1. 设置服务端口已经连接信息
  2. 创建一个特殊的客户端来保存我们的聊天室信息
  3. 编写可被客户端使用的聊天室的代码
  4. 创建一个manager类来管理我们的聊天室

2. 创建客户端

  1. 设置客户端的登录信息
  2. 初始化manager类来处理服务端消息
  3. 实现连接到聊天室的代码
  4. 实现发送聊天内容到其他客户端的代码

现在我们终于可以开始来编写我们的代码了。首先要创建的是我们的server类,并且让这个类继承自com.threerings.crowd.server.CrowdServer,就叫做HelloWorldServer.java,并且定义好main函数作为程序入口。

 

public class HelloWorldServer extends CrowdServer
{
  	public static void main (String[] args)
	{
		Injector injector = Guice.createInjector(new Module());
		HelloWorldServer server = injector.getInstance(HelloWorldServer.class);
		try {
			server.init(injector);
			server.run();
		} catch (Exception e) {
			log.warning("Unable to initialize server.", e);
		}
	}

 

 这里用到了GoogleGuice作为依赖注入的管理框架,这个可以先不去管它,除此之外还是很简单的。这样客户端就可以连接到服务端上了。server.init()和server.run()简单来说会创建一些像DObject manager等对象实例,并启动服务端消息发送和接收的线程。

 

 接下来我们要创建的是客户端对象,这个客户端对象首先是一个DObject,并且往往包含了一些客户端信息。当客户端连接到服务端的时候,服务端就会创建一个ClientObject的DObject,这个DObject保存了一些基本的客户信息,然后会被发送回客户端。所以当这个对象被修改时,客户端会被通知到。crowd框架在此基础上又继承了这个ClientObject,并把它叫做BodyObject。这个BodyObject又保存了一些额外的内容,比如用户名或其他一些状态信息,这样整个用户对象看起来更丰满更具体一些。在我们的聊天室应用当中,我们还需要在这个对象当中保存额外的聊天室编号列表信息,使得用户可以有选择的进入不同的房间。

 

 这可以分以下三步来实现。

  1. 通过继承BodyObject类来创建我们自己的ClientObject即ChatUserObject,并加入其他所需的类成员。
  2. 运行gendobj ant任务来生成DObject的代码。
  3. 通过ChatClientResolver类来负责创建ClientObject,即ChatUserObject。

 首先,创建ChatUserObject类继承自com.threerings.crowd.data.BodyObject,并加入一个声明为public的String数组,叫做roomids。这个roomids用来存放聊天室房间的编号,用户可以选择进入不同编号的房间。

 

public class ChatUserObject extends BodyObject
{
	public String[] roomIds;
}

 

 保存之后从命令行调用ant gendobj任务。这个gendobj ant任务会自动读取你的java源文件并生成对应的常数声明和方法。

 

 接下来需要告诉server使用我们自己的ChatRoomUser来取代BodyObject。这需要我们创建一个新的SessionFactory,并且重载它的两个抽象方法。我们的server类所继承的CrowdServer实例中包含了一个静态成员_clmgr,它就是我们的client manager。client manager负责创建和删除客户,使用SessionFactory获得client resolver,并通过client resolver创建具体的client object。目前来说,我们并不需要精通这其中的所有细节,我们只需要知道对于SessionFactory的getClientResolverClass方法我们需要返回我们自己定义的ChatClientResolver类,而对于getSessionClass方法只要返回框架默认的CrowdSession就可以了。这可以通过创建SessionFactory的匿名类,并把代码放在CrowdServer中的重载init方法内就可以了。

 

	public void init(Injector injector) throws Exception
	{

		super.init(injector);

		// configure the client manager to use our bits
        _clmgr.setDefaultSessionFactory(new SessionFactory() {
            @Override
            public Class<? extends PresentsSession> getSessionClass (AuthRequest areq) {
                return CrowdSession.class;
            }
            @Override
            public Class<? extends ClientResolver> getClientResolverClass (Name username) {
                return ChatClientResolver.class;
            }
        });

 

同时继承CrowdClientResolver类创建我们自己的resolver类ChatClientResolver。在这个类里边重载createClientObject方法来返回我们自己的ClientObject,即ChatUserObject。

 

public class ChatClientResolver extends CrowdClientResolver{

    @Override
    public ClientObject createClientObject ()
    {
        return new ChatUserObject();
    }
}

 

现在我们已经有了自己的clientObject类,我们还需要为我们的聊天用户提供一些房间。可以通过下面这些步骤来创建一个新的房间。 

  1. 创建一个PlaceView(view)作为客户端的界面。
  2. 创建一个placeController来控制面板(view)。
  3. 创建一个PlaceConfig来封装Controller的配置信息。
  4. 在服务端使用place registry创建房间的实例。

前面我们说过要使用经典的MVC模型来创建我们的聊天室应用,这里已经有了view跟controller,model作为数据存放在我们的ChatUserObject和另外一个叫做PlaceObject的对象中。PlaceObject也是一个DObject,在这个DObject里存放了一些多个客户端所共享的信息,这些信息可以用来更新和修改view。

 

PlaceView是一个可以与用户交互的GUI可视话组件。在我们的这个项目里,我们就使用swing来构建我们的view。我们将继承JPanel来摆放其他的一些控件,并且还需要实现PlaceView和ChatDisplay两个接口。PlaceView里面有两个方法,willEnterPlace和didLeavePlace。这两个方法允许你使用PlaceObject中的model数据来创建和销毁你的GUI界面。

 

ChatDisplay接口包含了两个方法,displayMessage和clear。在从其他客户端那里接收到聊天消息后,displayMessage方法会被调用,而clear方法则是在聊天消息需要被清除的时候被调用。这几个方法看起来还是比较简洁明了,容易理解的。

 

public class ChatRoomPanel extends JPanel implements PlaceView, ChatDisplay

 

如果你查看下ChatRoomPanel.java类文件,你会发现大部分都是一些基本的swing代码。其中添加了一个文本域作为显示聊天消息用,一个标签来显示当前的房间编号。当收到一条聊天消息时,我们提取出用户名和消息文本添加到文本域末尾。

 

下一步我们来创建controller。controller的作用是构造view,处理用户从PlaceView中的输入,当model被更新时更新我们的view。

 

controller中最重要的方法是createPlaceView,顾名思义,这个方法就是用来创建PlaceView的。但是这里容易混淆的是,除了createPlaceView方法之外我们还有一个叫做willEnterPlace的方法,该方法也是负责设立GUI的。但是这两个方法的区别是createPlaceView负责创建任何不需要model数据的界面元素,而willEnterPlace所负责创建的需要model数据。这里的model对象是一个DObject,即PlaceObject。

 

如果你观察我们的createPlaceView方法,我们向panel里添加了一大串的GUI组件。在该方法的末尾,我们调用了两个重要的方法addOccupantObserver和addChatDisplay。

 

_ctx.getChatDirector().addChatDisplay(panel)注册panel成为一个聊天显示面板。当在房间里的其他人发送了一条聊天消息的时候,这个panel会通过我们已经实现的ChatDisplay接口被通知到。

 

	@Override
	protected PlaceView createPlaceView (CrowdContext ctx)
	{
		// Create frame
		final ChatRoomPanel panel = new ChatRoomPanel();
		panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));

		_playerListModel = new DefaultListModel();
		_roomListModel = new DefaultListModel();

		final JList roomList = new JList(_roomListModel);
		final JList playerList = new JList(_playerListModel);

		......

		panel.add(new JScrollPane(playerList));
		panel.add(new JScrollPane(roomList));
		panel.add(moveButton);
		panel.add(chatInput);

		_ctx.getOccupantDirector().addOccupantObserver(this);
		_ctx.getChatDirector().addChatDisplay(panel);
		return panel;
	}

 

如果你仔细观察源码的话你会发现在controller中我们重载了PlaceView的willEnterPlace方法。这是因为我们希望PlaceView的当前可视状态匹配房间的当前状态。这就是说需要从PlaceObject的model数据中提取信息,在model内我们有一个成员变量occupantInfo。这个occupantInfo中包含了当前所有在该房间的用户。我们可以通过迭代把所有的用户列表显示到界面上,这样每当有用户进入房间时,他都能得到最新的用户列表。如果你之前仔细观察我们对OccupantObserver接口的实现,你也许会奇怪为什么这个接口不更新出一个初始的用户列表,这是因为OccupantObserver仅仅只是在用户进入该房间之后才调用接口方法。

 

 现在我们有了controller之后我们还需要一个PlaceConfig。这个PlaceConfig的任务是在客户端创建controller并且定义服务端的room manager。

 

我们只需要重载PlaceConfig的两个方法createController和getManagerClassName。如果你查看代码的话,这两个方法还是比较直白的。先不用担心那个room manager的类名,下面我会仔细介绍的。

 

public class ChatRoomConfig extends PlaceConfig
{
	@Override
	public PlaceController createController ()
	{
		return new ChatRoomController();
	}

	@Override
	public String getManagerClassName ()
	{
		return "net.tutorial.server.RoomManager";
	}
}

 

 一般来说当客户端请求move的时候,服务端首先验证客户端想要move的place,并发送回PlaceConfig。然后客户端调用PlaceConfig的createController方法来创建controller,再接着通过controller创建PlaceView。然后客户端从服务端接收PlaceObject(model数据)并传给controller的willEnterPlace方法。再然后controller创设model依赖对象并传给PlaceView的willEnterPlace方法,再创建出视图上的model依赖对象。

 

整个流程基本上是这样子的:

 

客户端请求move-->服务端处理该请求-->发回config到客户端-->客户端从config创建controller-->从controller创建出view

 

服务端发回model-->在客户端传给controller-->controller创建model依赖对象-->controller再将model传给view-->最后由view再创建出model依赖对象。

 

看起来内容有很多,不过却是经典的MVC实现。

 

下图是整个过程的时序图,左面方框表示客户端,右面方框代表服务端。客户端对服务端的方法调用是通过InvocationService来完成的。为了方便起见,图中对PlaceObject的subscribe部分做了简化,客户端对DObject的获取最终还是要到服务端上去取的。

 


 
 

 

接下来我们需要为我们的聊天室项目创建真正的客户类。我们的HelloWorldClient实现了一个叫做SessionObserver的接口,这个接口大家应该已经比较熟悉了,在前面几章有过介绍,可以让我们知道何时建立连接和断开连接。

 

login方法处理登录名及密码的创建以及调用底层的login方法。同时我们还创建了一个叫context的东西,这个等会也会讲到。

 

当我们的客户端连接到server之后,SessionObserver接口中的clientDidLogon方法会被调用,在这个方法中我们又调用了moveTo(2)方法。该方法请求server将我们移动到编号为2的房间中,这里的房间编号2即为placeObject的oid。一般来说我们不应该对oid硬编码,不过对于我们的这个例子来说这样做已经足够了。当我们的move请求被接受后,客户端开始创建一个新的view。

 

public class HelloWorldClient implements SessionObserver, RunQueue
{
	......
	public void init ()
	{
		......
		// log off when they close the window
		_frame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing (WindowEvent evt)
			{
				if (_ctx != null && _ctx.getClient().isLoggedOn()) {
					_ctx.getClient().logoff(true);
				} else {
					System.exit(0);
				}
			}
		});
	}

	public void login (String userName)
	{
		// Give me a random user name usign UUID
		Name name = new Name(userName);
		// Use a fakepassword because the server does not do any authentication
		// now.
		String password = "fakepassword";
		// Create a instance of our credentials to send to the server.
		UsernamePasswordCreds creds = new UsernamePasswordCreds(name, password);
		// Finally create our client and give it our credentials.
		Client client = new Client(creds, this);
		// For now use our localhost to run the server and client and default
		// ports.
		client.setServer("localhost", Client.DEFAULT_SERVER_PORTS);
		// Create a context object to give access to managers and helpers to
		// other classes
		_ctx = new HelloWorldContext(client, _frame);
		// Add a listener for logon logoff and other client events
		client.addClientObserver(this);
		// Finally logon
		client.logon();
	}

	// from RunQueue Interface
	public boolean isDispatchThread ()
	......

	// from RunQueue Interface
	public void postRunnable (Runnable r)
	......

	// from SessionObserver interface
	public void clientDidLogoff (Client client)
	......

	// from SessionObserver interface
	public void clientDidLogon (Client client)
	{
		log.info("(Step 2) Ya I logged on");
		// Move me to the start scene
		_ctx.getLocationDirector().moveTo(2);
	}

	// from SessionObserver interface
	public void clientObjectDidChange (Client client)
	......

	// from SessionObserver interface
	public void clientWillLogon (Client client)
	......
}

 

下一个我们所需要的是context类。context中包含的一些manager类使得我们可以在客户端中调用,在这里主要是我们的controller和view中。大家还记得前面我们说过客户端从config中创建出controller,再从controller中创建出view吗?这个时候创建出来的view就会被传到context中的setPlaceView方法中,在这里,这个placeView会被attach到窗体上,从而将GUI界面呈现在用户的面前。

 

public class HelloWorldContext implements CrowdContext
{

	public HelloWorldContext (Client client, JFrame frame)
	{
		_client = client;
		_locdir = new LocationDirector(this);
		_occdir = new OccupantDirector(this);
		_chatdir = new ChatDirector(this, null, null);
		_frame = frame;
	}

	public void setPlaceView (PlaceView view)
	{
		JPanel panel = (JPanel)view;
		_frame.getContentPane().removeAll();
		_frame.getContentPane().add(panel);
		_frame.repaint();
		panel.revalidate();
	}
	......
}	

 

 现在我们差不多已经创建了所有的客户端的类,我们再来看看在服务端上海需要实现哪些逻辑。

 

在crowd框架中,每当我们创建出一个place的时候,我们就要为它创建一个相应的manager,一个manager对应一个place。这个manager的任务就是处理所有的place相关的请求,也就是说大部分相关的逻辑都应该放在这里。在一会儿之前还记得我们在创建PlaceConfig的时候我们重载了一个叫做getManagerClassName的方法吗?返回的类名字串告诉我们的server,每当我们创建一个房间的时候,我们就为它创建这样一个manager类。默认的PlaceObject使用一个对应的叫做PlaceManager的对象。PlaceManager处理一些类似用户移动以及开启或者关停place的最基本的请求。我们的manager类继承自它,当然还要加入一些额外的逻辑了,比如我们希望我们的这个manager可以把所有可进入的房间编号呈现给用户,这样可以让用户选择可以进入哪些房间。

 

首先我们创建一个叫做RoomManager的类继承自PlaceManager。然后重载它的两个方法,分别为idleUnloadPeriod和bodyEntered。idleUnloadPeriod方法比较特殊,在房间里没有人的时候它可以把房间关掉一会儿。在我们的应用里就不需要搞的这么复杂了,我们就直接返回0,意思是不需要它自动关停。

 

在客户端调用moveTo方法进入房间的时候bodyEntered方法会被调用。bodyOid是进入该place的BodyObject的oid。我们可以通过_registry.enumeratePlaces方法来迭代取得所有的房间对象。我们从房间对象中获取oid并存入一个数组中。然后通过调用player.setRoomIds(roomIdList)方法把房间列表传给我们的客户端用户。这个方法首先会更新服务端的本地变量值然后把结果再发送给我们的客户端。

 

public class RoomManager extends PlaceManager
{
	@Override
	protected long idleUnloadPeriod ()
	{
		return 0;
	}

	@Override
	protected void bodyEntered (int bodyOid)
	{
		super.bodyEntered(bodyOid);

		Iterator itr = _registry.enumeratePlaces();
		String[] roomIdList = new String[0];

		while (itr.hasNext()) {
			PlaceObject room = (PlaceObject)itr.next();
			roomIdList = ArrayUtil.append(roomIdList, String.valueOf(room.getOid()));
		}

		ChatUserObject player = (ChatUserObject)PresentsServer.omgr.getObject(bodyOid);
		player.setRoomIds(roomIdList);
	}
}

 

现在每次用户进入房间之后都会得到一串房间编号列表。有一个地方要注意的是bodyEntered方法是在createPlaceView方法之后被调用的,所以在存取player数据的时候要小心些。

 

现在剩下最后一步就是来创建一些房间。在我们的HelloWorldServer的init方法中我们调用了PlaceRegistry的createPlace(new ChatRoomConfig())方法。PlaceRegistry的任务是用来在服务端记录跟踪并创建所有的place,同时为每一个place创建并初始化一个相应的PlaceManager。

 

	@Override
	public void init(Injector injector) throws Exception
	{
		...
		// create some chat rooms
		_plreg.createPlace(new ChatRoomConfig());
		_plreg.createPlace(new ChatRoomConfig());
		_plreg.createPlace(new ChatRoomConfig());
	}

 

现在已经万事俱备,我们只要打开一个命令行运行ant server来启动服务器,再打开一个命令行运行ant client启动一个客户端应用程序。输入你的用户名点击login就可以了。你现在可以在底部的输入行输入想要的聊天消息,还可以选择进入不同的房间来聊天。

 

不过这里要注意的是如果有一个用户使用和你相同的ID登录的话,先前登录的那个用户会被自动注销,这是因为我们在用户登录的时候并没有实现任何的验证程序。还有一点就是当你的客户端应用崩溃或者掉线的时候,用户连接并不会马上断开,这是因为Threerings框架允许在你机器宕机或者掉线的时候可以重新连接到原先的session中,但是如果一旦掉线时间过长的话,就无法重新连接回去了。

 

 

  • 大小: 70 KB
分享到:
评论
1 楼 萧十一狼 2010-05-11  
/*这段代码是别人的,自己写太麻烦,为读到这篇文章的读者,稍微介绍下googleGuice
纯净的Ioc容器,用spring也能做,但仅仅为了Ioc用spring又有很多用不到的功能。
google 的新Ioc容器 号称快spring100倍,貌似spring2.5说自己的速度现在也很
快*/    
   
//小例子:    
   
public interface Service {    
void go();    
}    
   
   
   
public class ServiceImpl implements Service {    
public void go() {    
   System.out.println("go go go a le a le a le");    
}    
}    
   
  
   
public class Client {    
private final Service service;    

//注意到@Inject这个Annotation就是告诉Guice,这里需要注入
@Inject   
public Client(Service service) {    
   this.service = service;    
}    
public void go(){    
   service.go();    
}    
}    
   
 
   
public class MyModule implements Module {    
public void configure(Binder binder) {    
   //用ServiceImpl.class注入Service中需要注入的地方
   binder.bind(Service.class).to(ServiceImpl.class).in(Scopes.SINGLETON);    
}    
}    
      
   
public static void main(String[] args) {
    
   Module myModule = new MyModule();    
   Injector injector = Guice.createInjector(myModule);    
   Client client = injector.getInstance(Client.class);    
   client.go();    
}    

相关推荐

Global site tag (gtag.js) - Google Analytics