📜 ⬆️ ⬇️

Creating games without Canvas

One day, Blizzard's HeartStone card game caught my eye. Playing it came the idea that such things can be created using html5 technology, which will allow them to be cross-platform. In my opinion, such things can be done by people who are still engaged only in creating websites.

So, what we have:


Actually, everything. This is quite enough to carry out our plans.

For this example, I used a ready-made WebSockets server written in php.
')
Exchange with the server will be data in json format to transfer the names of methods and arguments.

The most basic thing we need is to establish the sane interaction between the client and the server, that is, the interface through which our application (in this case, the game) can be expanded.

In the client, it will look like this:

var socketInfo = {}; socket = new WebSocket('ws://localhost:8000'); socket.onopen = function (e){ socketInfo.method = "connect"; socket.send(JSON.stringify(socketInfo)); console.log('   ws://localhost:8000'); } socket.onclose = function (e){ console.log(' !'); } socket.onmessage = function (e){ if (typeof e.data === "string"){ var request = JSON.parse(e.data); Actions[request.function](request.args); }; } 

This is a standard description of WebSockets features. Let’s focus on the onmessage method.

As a parameter, it takes a string in json format, which was sent to us by the server. The string contains the name of the function that must be executed on the client, as well as the parameters that we will pass to it.

In order to run a function whose name contains some variable, this function must be an element of an array (alternatively, the global window array). In this example, this is the Actions array:

  var Actions = { myId: function(id){ localStorage.setItem("myId", id); }, log: function(str){ console.log(str); }, ....... } 

Naturally, we also send a string to the server in json format. I have two fields: the name of the method and the argument.
Similarly, the server parses the resulting line:

 function websocket_onmessage($keyINsock, $str){ global $Users; $json = json_decode($str); $method = strval($json->{'method'}); $args = $json->{'args'}; if (!isset($args)) $args = $keyINsock; if (!empty($method)) $Users->$method($keyINsock, $args); } 


The websocket_onmessage function processes requests to our server, taking as an argument the connection ID and the transmitted string, respectively. Next comes the work with the object of the class Users.

Let me explain using the example of connecting players:

  public function frAccept($myid, $opId){ $this->opponents[$myid] = $opId; $this->opponents[$opId] = $myid; $args = array($myid, $opId); $arrOut = array('function' => 'frAccept', 'args' => $args); $arrOutJSON = json_encode($arrOut); websock_send($opId, $arrOutJSON); websock_send($myid, $arrOutJSON); } 

In each method, the first parameter is the connection identifier for which the request came, which is why it is called $ myId. The example describes the method of connecting two players (the second parameter is the id of the player who accepted the invitation to battle). For simplicity, I do not use databases, so we put the IDs of the oponients into an array for further work with them (they are stored in the client in LocalStorage, but why drive them back and forth).

All those whose opponent wrote down, give this information to both players.

  frAccept: function(id){ fight_start(id); console.log('Fight starting'); } ....... function fight_start(ids){ $('body').load('fight.html'); curplayer = ids[0]; localStorage.setItem("opponent", ids[0]); if (ids[0] == localStorage.getItem("myId")) localStorage.setItem("opponent", ids[1]); } 

The variable curplayer stores data about whose turn it is.

Our whole game is built on ordinary blocks div.

 <meta http-equiv="Cache-Control" content="no-cache" /> <div id="to_area"></div> <div id="wrapper"> <div id="opHand"></div> <div id="area"> <div id="timer"> <div class="bg"></div> </div> <div id="opUnits"> <div class="nexus card" data-health="30" id="" data-attack="0"> <div class="health"> <div class="inner">30</div> </div> </div> <div class="bg"></div> </div> <div id="myUnits"> <div class="nexus card" data-health="30" data-attack="0"> <div class="health"> <div class="inner">30</div> </div> </div> <div class="bg"></div> </div> <div id="next"> </div> </div> <div id="myhand"></div> </div> 


Having such a frame, we begin to work with the elements of the game.

To get started, we get 4 starting cards. How to do it? Four times we ask the server: “give the card”. I simplified this operation by adding the desired number of cards as an argument: get_cards (4);

  function get_cards(num){ socketInfo.method = "get_cards"; socketInfo.args = num; socket.send(JSON.stringify(socketInfo)); } 

We have already figured out that the server parses the received json string in the name of the method and the argument. As a result, we will use the get_cards method:

  public function get_cards($myid, $num){ global $Cards; for ($i=0; $i < $num; $i++){ $card = $Cards->getRandom(); $args = array( "player_id" => $myid, "card" => $card ); $arrOut = array('function' => 'player_get_card', 'args' => $args); $arrOutJSON = json_encode($arrOut); websock_send($myid, $arrOutJSON); websock_send($this->opponents[$myid], $arrOutJSON); } } 

In the loop, we request a random card from the deck ($ Cards-> getRandom () returns the json card representation: name, attack, number of lives, picture) and send the result to both players. Do not forget about the opponent, he should see that we took a map.

Which players took the current map is determined by comparing the ID:

  player_get_card: function(args){ if (!me(args.player_id)) { $('#opHand').append('<div class="card" id="'+args.card.id+'" />'); return; }else{ var card = card_construct(args.card); $('#myhand').append(card); return; } } 

Accordingly, #opHand is an opponent’s hand, #myhand is our hand. That is, the cards that we hold in hand.

So, the cards are typed, it's time to lay them on the table. To do this, we write an elementary jquery function:

  $('#myhand').on('click',".card",function(){ //      if (!me(curplayer)) return; to_area($(this)); }); 

When clicking on the map in our hand, the map is sent to the arena: to_area ().

  function to_area(card){ //     card.appendTo('#myUnits'); socketInfo.method = 'opGetCard'; socketInfo.args = card.attr("id"); socket.send(JSON.stringify(socketInfo)); } 

Do not forget that the enemy must see all this.

  public function opGetCard($myid, $card_id){ global $Cards; $card = $Cards->getById($card_id); $arrOut = array('function' => 'opGetCard', 'args' => $card); $arrOutJSON = json_encode($arrOut); websock_send($this->opponents[$myid], $arrOutJSON); } 


Next, you need to consider the most important - attack. One card, we attack the enemy card. For this we need:


To select the card-aggressor, we will manipulate the active class:

  $('#area').on('click',"#myUnits .card",function(){ //////////////////      if(!me(curplayer) || $(this).hasClass('nexus'))return; if ($(this).hasClass('active')) { $(this).removeClass('active'); }else{ $('#area #myUnits .card.active').removeClass('active'); $(this).addClass('active'); } }); 

The aggressor has a class of active, that is, the card is selected. Now you can choose a victim.

  $('#area').on('click',"#opUnits .card",function(){ //////////////////       $agressor = $('#area #myUnits .card.active'); if ($agressor.length < 1) { return false; }; attack_request($agressor,$(this)); }); 

So, when clicking on the victim map, we pass two arguments to the function with the talking name attack_request: the aggressor and the victim. That is who and who will attack.

The function itself is a wrapper and only sends information to the server that there will be an attack.

  function attack_request(agressor, victim){ ////////////   socketInfo.method = 'attack_request'; socketInfo.args = agressor.attr("id")+'-'+victim.attr("id"); socket.send(JSON.stringify(socketInfo)); } 

The function on the server works in echo mode, sends the information back to me and my opponent. This is done in order to level out the difference in Internet speed between two players as much as possible (after all, we have a timer in the game that counts 2 minutes per turn).

  public function attack_request($myid, $args){ $arrOut = array('function' => 'attack', 'args' => $args); $arrOutJSON = json_encode($arrOut); websock_send($myid, $arrOutJSON); websock_send($this->opponents[$myid], $arrOutJSON); } 

Our cards are clearly identified with unique IDs, therefore, having the aggressor and victim id, we can draw this attack:

  attack: function (args){ id = args.split('-'); var alias = $('#'+id[0]).data('alias'); onBeforeAttack(alias,id); } .................. function onBeforeAttack(alias, id){ //     if(Units[alias] == undefined){ var uFile = 'js/units/'+alias+'.unit.js'; var xmlhttp = getXmlHttp(); xmlhttp.open('GET', uFile, false); xmlhttp.send(null); if(xmlhttp.status == 200){ $('body').append('<script src="'+uFile+'"></scipt>'); }else{ Units[alias] = new defaultUnit(); } } Units[alias].attack(id[0], id[1]); } 


Please note that before the attack is drawn it is necessary to check whether we have an object containing information about this type of unit. I tried to immediately think about the moment of extensibility. After all, we have both melee units and range, and we would like their attack to look different. But this is for the future. So far we have only the default object that is used if the corresponding file with the description of the unit object is missing.

Here is what it looks like:

  function defaultUnit(){} defaultUnit.prototype.attack = function(agressor_id, victim_id){ var victim = $('#'+victim_id); var agressor = $('#'+agressor_id); victim.css('z-index',5); agressor.css('z-index',10); var $ypos = victim.offset().top - agressor.offset().top; var $xpos = victim.offset().left - agressor.offset().left; agressor.animate({top:$ypos, left:$xpos}, 100).delay(200).animate({top:0, left:0}, 100); if (isNaN(victim.data('attack'))) victim.data('attack',0); if (isNaN(agressor.data('attack'))) agressor.data('attack',0); victim.data('health',victim.data('health')-agressor.data('attack')); agressor.data('health',agressor.data('health')-victim.data('attack')); refreshCards(); } 

After the attack is drawn, we call refreshCards (), which will change the values ​​of the number of lives of all the cards in the arena (just everyone, as there are massive spells).

  refreshCards = function(){ ////    $('#myUnits .card.active').removeClass('active'); var rest = setTimeout(function(){ $('#area .card').each(function(){ refresh_card($(this)); if ($(this).data('health') < 1) { death($(this)); }; }); clearTimeout(rest); },1000); } function refresh_card(card){ //////////   $('.health .inner',card).html(card.data('health')); $('.attack .inner',card).html(card.data('attack')); } 

If the number of lives becomes less than one, we launch the “dying” card, and if the main building dies, which, in essence, the game, must be defended, then we also trigger the event of losing one of the players:

  function death(unit){ //////////  if (unit.hasClass('nexus')) { var lose = localStorage.getItem("opponent"); if (!me(curplayer)) lose = localStorage.getItem("myId"); socketInfo.method = 'lose'; socketInfo.args = lose; socket.send(JSON.stringify(socketInfo)); }; unit.addClass('die'); var dt = setTimeout(function(){ unit.remove(); clearTimeout(dt); },2000); } 

Actually, I was not going to describe every elementary action that I created in the game, described the concept.

To view the demo you will need two browsers / tabs (it looks rotten on mobile, I do not advise it).

141.8.196.181/new-cards

Source: https://habr.com/ru/post/252897/


All Articles