📜 ⬆️ ⬇️

Implementing a simple pixel game in the Ethereum blockchain

Hello! Inspired by r / place and wanting to finally realize our first smart contract on the blockchain, we decided to make everyone an accessible and fun application on the Ethereum network that allows you to draw on a canvas of 1000 x 1000 px, keeping each pixel selected and colored by the user in blockchain You can also draw in real time with your friends and observe how the color of the selected pixel changes in real time as the smart contract's transactions are confirmed on the network.

The smart contract does not require payment for changes in the color of the pixels, but you will need to pay a small commission to the miners for confirming the transaction.

In this article I would like to tell you how we got the current first version of the application and what technical difficulties we had to face.

Components


Planning to focus more on writing a smart contract, we did not want to spend a lot of time on the server architecture and made everything as simple as possible.
The main condition for working with the application is an Ethereum-compatible browser and browser plug-in (Metamask etc) with access to the wallet. In this case, the client sends the transactions to the blockchain itself and the server only returns the updated contract status via Nginx. Thus, the entire integrity and security of data is guaranteed by a smart contract in the blockchain.
')
image

Thus, we have turned out:


Smart contract


First of all, we took up the implementation of the smart contract and taking into account our initial experience in working with Solidity, and relying on the best practices available in the network, we decided to go through MVC and make a model for storing the contract state and controller that can update this model.

The biggest plus of this approach is that in the future, if there are changes and as new functionality appears, we do not need to migrate the application state and only the controller needs to be updated.

The only disadvantage of this approach is the high cost of the smart contract when it is initialized on the Ethereum network (about $ 1,100 at that time).

Naive implementation


Initially, we did not think about the effectiveness of our implementation and premature optimization, so we made the first contract as clumsy as possible. Considering the size of our canvas for drawing at 1000px x 1000px, we got an array of 1 million uints in which we stored all the pixels.

An example of the implementation of the contract below:

contract PixelsField is Controllable { uint[1000000] public colors; event PixelChanged(uint coordinates, uint clr); function setPixel(uint coordinates, uint clr) public onlyController { require(clr < 16); require(coordinates < 1000000); colors[coordinates] = clr; PixelChanged(coordinates, clr); } function allPixels() public view returns (uint[1000000]) { return colors; } } 

Difficulties


At first glance, everything worked just fine, we were able to save the first pixels in the blockchain, but as soon as we tried to read the state from the network, we were in for a huge disappointment.


Improved version


Being upset by the failure in the first version of implementation, we began to think about a possible algorithm for compressing and reading pixels, how to make a more compact version of the array, and also save one of the possible 16 colored pixels there.

Having carefully studied the data types available in Solidity, we decided to stay with uint256, but we started recording both the position and the color of the pixel in it, thereby hoping to place each pixel in 4 bit of the 256 available.

To do this, we needed a bit of bitwise magic, in which we encode the X and Y coordinates of each pixel into the index of the array element and apply a bit shift and mask.

An example implementation below:

 function setPixel(uint coordinate, uint color) public onlyController { require(color < 16); require(coordinate < 1000000); uint idx = coordinate / ratio; uint bias = coordinate % ratio; uint old = colors[idx]; uint zeroMask = ~(bitMask << (n * bias)); colors[idx] = (old & zeroMask) | (color << (n * bias)); PixelChanged(coordinate, color); } 

A quick calculation of our implemented optimization showed that in order to store 1 million pixels, we would need only 1,000 0000 / 64bit = 15,625 elements of the uint256 type.

Thus, we reduced the initial array from our naive implementation 64 times and were able to read the entire array in a reasonable time on the client.

A full example of a state contract is below:

 contract PixelsField is Controllable { event PixelChanged(uint coordinates, uint clr); uint[15625] public colors; uint bitMask; uint n = 4; uint ratio = 64; function PixelsField() public { bitMask = (uint8(1) << n) - 1; } function setPixel(uint coordinate, uint color) public onlyController { require(color < 16); require(coordinate < 1000000); uint idx = coordinate / ratio; uint bias = coordinate % ratio; uint old = colors[idx]; uint zeroMask = ~(bitMask << (n * bias)); colors[idx] = (old & zeroMask) | (color << (n * bias)); PixelChanged(coordinate, color); } function getPixel(uint coordinate) public view returns (uint) { var idx = coordinate / ratio; var bias = coordinate % ratio; return (colors[idx] >> (n * bias)) & bitMask; } function allPixels() public view returns (uint256[15625]) { return colors; } } 

Contract Interaction


To interact with the contract from the UI, we added the following functions that have access to the status of the contract:

 function getPixel(uint coordinate) public view returns (uint) function allPixels() public view returns (uint256[15625]) 

User interface


Our goal was to make the UI as simple and easy as possible, where the user focuses on the canvas for drawing and from the available tools there is only a choice of colors and the possibility of zoom.

Rendering the entire canvas is a fairly quick and inexpensive operation, primarily because of the compact size of the array of pixels in the blockchain and the use of Canvas in the browser.

Moreover, we took into account that among visitors there may also be those who do not have an Ethereum-compatible browser or browser plug-in (Metamask etc), so we allowed our server to generate the current state of all pixels on the canvas from the blockchain and give the client a static image via nginx.

To repaint a pixel, we use the web3.js library. The function call from the contract is shown below:

 const colorSelected = (color) => () => { hidePicker(); web3.eth.getAccounts((_error, accounts) => { if (accounts.length === 0) { alert("Please login in you wallet. Account not found ¯\_(ツ)_/¯."); return; }; const config = { from: accounts[0], gasPrice: 2500000000, gasLimit: 50000, value: 0 }; try { controllerContract.methods.setPixel(settings.selectedcoordinate, color).send(config, (error, addr) => { if (error) { console.log(error); return; } userPixels.push({ coord: settings.selectedcoordinate, color: color }); const {x, y} = numberToCoord(settings.selectedcoordinate); setPixel(ctx, x, y, settings.colors[color]); }); } catch (error) { console.log(error); } }); } 

Server


The implementation of the API was not our priority, as we hoped to rely entirely on the client’s web3.js library.

But since there are a number of users of browsers without Ethereum compatible plug-ins and mobile devices, we decided to raise our Parity nodes in the DigitalOcean environment and synchronize it with the network.

In order to interact with Parity, we wrote a lightweight API that poll the Parity node, takes the current state of the contract and draws this latest state, saving the image in png format on the server. Then it’s Nginx’s concern to give the picture to the client.

Since the contract state is an array of uint256 data, the approximate payload of what we get from the contract looks like this:
0x0000000000000000000000000000000000000000000000000000000000000b ...

And we have to do the transformation given our 16 available colors on the client and the required result in the form of png pictures:

Example below:

 mport java.awt.{Color => AwtColor} import java.io.{File, FileOutputStream} import java.time.Instant import com.sksamuel.scrimage.nio.PngWriter import com.sksamuel.scrimage.{Image, Pixel} import com.typesafe.scalalogging.StrictLogging import org.web3j.utils.{Numeric => NumericTools} import scala.util.Try object Composer extends StrictLogging { private lazy val colorMapping: Map[Char, String] = Map(  '0' -> "#FFFFFF",  '1' -> "#9D9D9D",  '2' -> "#000000",  '3' -> "#BE2633",  '4' -> "#E06F8B",  '5' -> "#493C2B",  '6' -> "#A46422",  '7' -> "#EB8931",  '8' -> "#F7E26B",  '9' -> "#2F484E",  'a' -> "#44891A",  'b' -> "#A3CE27",  'c' -> "#1B2632",  'd' -> "#005784",  'e' -> "#31A2F2",  'f' -> "#B2DCEF") private lazy val pixelsMapping: Map[Char, Pixel] = hex2Pixels(colorMapping) private val canvasHeight = 1000 private val canvasWidth = 1000 private val segmentLength = 64 def hex2Pixels(map: Map[Char, String]): Map[Char, Pixel] = {  def pixel(hex: String) = {    for {      color <- Try(AwtColor.decode(hex)).toOption      pixel = Pixel(color.getRed, color.getGreen, color.getBlue, 255)    } yield pixel  }  for {    (color, hex) <- map    pixel <- pixel(hex)  } yield color -> pixel } def apply(encoded: String): Unit = {  val startedAt = Instant.now  val pixels = translateToPixels(encoded)  write(pixels, fileName)  logger.info(s"Successfully wrote $fileName, took ${ Instant.now.toEpochMilli - startedAt.toEpochMilli } ms") } def translateToPixels(encoded: String): List[Pixel] = {  def decode(color: Char) = for (pixel <- pixelsMapping.get(color)) yield pixel  val extracted = NumericTools.cleanHexPrefix(encoded)  extracted.grouped(segmentLength)    .toList    .par    .flatMap(_.reverse.toSeq)    .flatMap(decode)    .toList } private def write(pixels: List[Pixel], fileName: String): Unit = {  val file = new File(fileName)  val out = new FileOutputStream(file, false) // don't append existing file  val image = Image(canvasWidth, canvasHeight, pixels.toArray)  val pngWriter = PngWriter()  pngWriter.write(image, out)  out.flush()  out.close() } } 

Result


What have learned:


Summing up, it was very interesting and informative for us to make the first application on the blockchain and put it on mainnet and we hope that it will also be exciting for users to try to draw something on the blockchain and keep it in history :)

Future plans


We are going to develop ethplace.io and will be happy to share with you soon news about new interesting features we are working on!

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


All Articles