preview
Programación gráfica para principiantes (Parte I): Aprendiendo CCanvas con Crazy Scalper

Programación gráfica para principiantes (Parte I): Aprendiendo CCanvas con Crazy Scalper

MetaTrader 5Ejemplos |
51 0
Dayana Cubillas Massana
Dayana Cubillas Massana

Introducción: El poder de aprender jugando

Usted ya programa EAs e indicadores en MQL5, pero al llegar a la parte gráfica con CCanvas suele aparecer el “pantallazo negro”: coordenadas contraintuitivas (Y aumenta hacia abajo), dudas sobre cómo crear un lienzo transparente ARGB, cómo actualizar la pantalla sin depender de ticks y cómo captar eventos de teclado. Sin esos conocimientos, las interfaces quedan estáticas, con parpadeos o con lógica dispersa de flags y timers.

En este artículo abordamos exactamente ese problema: construiremos un proyecto interactivo mínimo —Crazy Scalper— que se ejecuta sobre el gráfico de MetaTrader 5 y sirve como plantilla práctica. Aprenderá a crear correctamente la capa gráfica (CreateBitmapLabel con formato ARGB), a montar un bucle de actualización estable (EventSetMillisecondTimer), a dibujar con geometría vectorial, a implementar física básica y colisiones, y a procesar eventos de teclado. El objetivo es que, una vez completado, disponga de un armazón reutilizable para añadir paneles y elementos interactivos en sus propios EA.


El Paradigma del Lienzo: Entendiendo el Sistema de Coordenadas

Antes de escribir código debemos entender cómo "piensa" una pantalla de ordenador. Como traders estamos acostumbrados a los gráficos financieros: si el precio sube, la coordenada vertical Y aumenta. Sin embargo, en la programación gráfica tradicional y en CCanvas, las reglas cambian.

Imagine que su monitor es una hoja de papel cuadriculado. En CCanvas, el punto de origen —es decir, la coordenada (0,0)— no está abajo, sino en la esquina superior izquierda de la pantalla.

  • A medida que nos movemos hacia la derecha, el valor de X aumenta.
  • A medida que nos movemos hacia abajo, el valor de Y aumenta.

Este concepto es vital. Más adelante, cuando programemos la gravedad del cohete para que caiga, lo que haremos será sumar valores a su coordenada Y. Y para que vuele hacia arriba (un salto) vamos a restarle valores a su coordenada Y.


Preparando el Lienzo: El Cristal Transparente y el Canal Alpha

Para empezar a dibujar necesitamos crear el espacio de trabajo, en MQL5, primero incluimos la librería principal con #include <Canvas\Canvas.mqh> y creamos una instancia global llamada canvas.

El primer paso lógico dentro de nuestra función OnInit() es ocultar las típicas velas japonesas y la cuadrícula del terminal para dejar la pantalla totalmente limpia. Esto lo logramos desactivando la propiedad del gráfico mediante ChartSetInteger(0, CHART_SHOW, false). Ahora con el fondo limpio, obtenemos el ancho y alto actuales de la ventana del usuario usando píxeles.

Entonces viene el momento crítico: la creación del lienzo mediante la función CreateBitmapLabel. Piense en esta función como si estuviéramos colocando un cristal encima del gráfico de MetaTradrr 5, y sobre este cristal vamos a pintar.

Uno de los secretos para lograr gráficos modernos e integrados es el último parámetro de esta función, el formato de color. La mayoría de desarrolladores utilizan formatos de color opacos por defecto. Nosotros utilizaremos COLOR_FORMAT_ARGB_NORMALIZE.

¿Qué significa ARGB? Son las siglas de Alpha, Red, Green, Blue (Alpha, rojo, verde, azul). El canal Alpha es el encargado de la opacidad, y se mide con valores del 0 al 255:

  • Un Alpha de 255 es un color totalmente sólido.
  • Un Alpha de 0 significa que el color es totalmente invisible.
  • Un Alpha intermedio (ej. 150) nos permite crear sombras, marcas de agua y en nuestro caso el fuego translúcido del propulsor de nuestro cohete.

Veamos cómo se traduce todo esto en el código de inicialización de nuestro proyecto:

//--- Include the standard MQL5 library for rendering graphics on the screen
#include <Canvas\Canvas.mqh>

//+------------------------------------------------------------------+
//| Global Variables for the Graphical Engine                        |
//+------------------------------------------------------------------+
CCanvas        canvas;                  // The main object that will act as our drawing canvas
int            g_chartWidth;            // Will store the actual chart window width in pixels
int            g_chartHeight;           // Will store the actual chart window height in pixels
bool           g_isInitialized = false; // Security flag to prevent timer execution before initialization

//+------------------------------------------------------------------+
//| Game State Machine                                               |
//+------------------------------------------------------------------+
enum ENUM_GAME_STATE
  {
   START_SCREEN,                        // Initial screen waiting for user input
   PLAYING,                             // Active gameplay state
   GAME_OVER                            // Liquidation screen (Margin Call)
  };
ENUM_GAME_STATE g_gameState = START_SCREEN;

//+------------------------------------------------------------------+
//| Physics and Movement Variables                                   |
//+------------------------------------------------------------------+
double         g_playerY;               // Current vertical position of the rocket
double         g_playerX;               // Current horizontal position of the rocket
double         g_velocityY;             // Current falling speed
const double   GRAVITY = 0.6;           // Constant downward force applied every frame
const double   JUMP_STRENGTH = -9.5;    // Upward force (negative Y) applied on key press
int            g_score = 0;             // Current game score (Profit)
int            g_bestScore = 0;         // All-Time High score

//+------------------------------------------------------------------+
//| Obstacles (Japanese Candles) Structure and Variables             |
//+------------------------------------------------------------------+
struct CandlePipe
  {
   double            x;                 // Horizontal position of the candle obstacle
   double            gap_y;             // Vertical center of the safe gap for the rocket
   bool              passed;            // Flag to avoid counting the same obstacle twice
  };
CandlePipe     g_pipes[3];              // Array to hold a maximum of 3 obstacles on screen

double         g_pipeSpeed;             // Current moving speed of the obstacles
int            g_pipeGap;               // Vertical space between the upper and lower candles
int            g_pipeSpacing;           // Horizontal distance between one obstacle and the next

//--- Base difficulty constants
const double   BASE_PIPE_SPEED = 6.0;   // Starting speed
const int      BASE_PIPE_GAP = 220;     // Starting vertical gap
const int      BASE_PIPE_SPACING = 450; // Starting horizontal distance
const int      PIPE_WIDTH = 45;         // Fixed width of the candlestick obstacles

//--- Variable to animate the background grid (Parallax effect)
double         g_gridOffsetX = 0.0;

//+------------------------------------------------------------------+
//| Cyberpunk Color Palette using Alpha Channel (ARGB)               |
//+------------------------------------------------------------------+
uint CLR_BG       = ColorToARGB(C'15,17,20', 255);   // Dark solid background (Alpha 255)
uint CLR_GRID     = ColorToARGB(C'25,28,32', 255);   // Grid lines color
uint CLR_ACCENT   = ColorToARGB(C'0,240,240', 255);  // Neon Cyan for details
uint CLR_BULL     = ColorToARGB(C'40,167,69', 255);  // Institutional Green (Bull candle)
uint CLR_BEAR     = ColorToARGB(C'242,54,69', 255);  // Institutional Red (Bear candle)
uint CLR_ROCKET   = ColorToARGB(C'255,160,0', 255);  // Orange/Gold for the rocket body
uint CLR_FIRECORE = ColorToARGB(C'255,255,100', 255); // Yellow for the engine fire

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize the random number generator using the system tick count
   MathSrand(GetTickCount());
   
//--- Hide the standard MT5 candlestick chart to provide a clean visual space
   ChartSetInteger(0, CHART_SHOW, false);
   
//--- Get the actual screen dimensions in pixels from the user's terminal
   g_chartWidth  = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   g_chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   
//--- Create the canvas using COLOR_FORMAT_ARGB_NORMALIZE to support true Alpha transparency
   if(!canvas.CreateBitmapLabel(0, 0, "FlappyScreen", 0, 0, g_chartWidth, g_chartHeight, COLOR_FORMAT_ARGB_NORMALIZE))
     {
      return(INIT_FAILED);
     }

//--- Setup initial game variables
   ResetGame();
   
//--- Set a high-frequency timer to 12 milliseconds (approx. 83 Frames Per Second)
   EventSetMillisecondTimer(12);
   g_isInitialized = true;
   
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Stop the game loop
   EventKillTimer();
   
//--- Destroy the canvas to free up RAM
   canvas.Destroy();
   
//--- Restore the standard MT5 chart view
   ChartSetInteger(0, CHART_SHOW, true);
  }

//+------------------------------------------------------------------+
//| Reset the game to initial state                                  |
//+------------------------------------------------------------------+
void ResetGame()
  {
//--- Position the rocket vertically in the center and horizontally to the left
   g_playerX = g_chartWidth * 0.2; 
   g_playerY = g_chartHeight / 2.0;
   g_velocityY = 0.0;
   g_score = 0;
   
//--- Restore base difficulty parameters
   g_pipeSpeed   = BASE_PIPE_SPEED;
   g_pipeGap     = BASE_PIPE_GAP;
   g_pipeSpacing = BASE_PIPE_SPACING;
   
//--- Generate the first 3 obstacles outside the right edge of the screen
   for(int i = 0; i < 3; i++)
     {
      g_pipes[i].x = g_chartWidth + (i * g_pipeSpacing);
      g_pipes[i].gap_y = (double)(MathRand() % (g_chartHeight - 300) + 150); 
      g_pipes[i].passed = false;
     }
  }


La Máquina de Estados (State Machine)

Observe cómo en el código superior hemos declarado un enum llamado ENUM_GAME_STATE. Los programadores novatos suelen utilizar múltiples variables booleanas para controlar lo que ocurre en la pantalla. Esto genera errores lógicos muchas veces.

Al usar un enum creamos una máquina de estados finitos. Nuestro juego solo puede existir en uno de los tres estados: en la pantalla de inicio, en medio de una partida o en la pantalla de liquidación. Esto mantendrá nuestro código ordenado a medida que avancemos.


El Latido del Juego: El Bucle Principal (Game Loop)

En el trading algorítmico, los EAs suelen reaccionar a un nuevo tick (OnTick) o a la formación de una nueva vela. Sin embargo, un videojuego o un panel gráfico dinámico no puede esperar a que el mercado se mueva. Necesita actualizarse constantemente, incluso los fines de semana cuando el mercado está cerrado.

Para solucionar esto, en nuestra función de inicialización usamos EventSetMillisecondTimer(12). Esto le dice a MetaTrader 5: ejecuta la función OnTimer() cada 12 milisegundos. Si dividimos 1 segundo entre 12, obtenemos aproximadamente 83 fotogramas por segundo (FPS). Este es el latido de nuestro juego.

Dentro de OnTimer(), ocurre la magia. Todo motor gráfico (o la mayoría) sigue un ciclo de tres pasos en una fracción de segundo:

  1. Borrar: Se limpia el lienzo. Si no borráramos, los dibujos se amontonarían dejando una estela infinita en la pantalla.
  2. Calcular y dibujar: Evaluamos en qué estado de nuestra máquina de estados estamos (Inicio, Jugando, Game Over) y actualizamos las posiciones matemáticas de los objetos para luego dibujarlos.
  3. Actualizar pantalla: Finalmente llamamos a canvas.Update(). Esta es la orden que le dice a MetaTrader 5 que tome nuestro lienzo interno y lo muestre físicamente en el monitor del usuario.
//+------------------------------------------------------------------+
//| Timer function (Main Game Loop)                                  |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Prevent execution if initialization failed
   if(!g_isInitialized)
     {
      return;
     }

//--- Check if the user resized the MT5 window
   int curr_w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int curr_h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   
   if(curr_w != g_chartWidth || curr_h != g_chartHeight)
     {
      g_chartWidth = curr_w; 
      g_chartHeight = curr_h;
      canvas.Resize(g_chartWidth, g_chartHeight);
     }

//--- STEP 1: CLEAR THE SCREEN (Erase the previous frame to avoid infinite trails)
   canvas.Erase(CLR_BG);
   
//--- Calculate the Parallax background offset
   if(g_gameState == PLAYING)
     {
      g_gridOffsetX -= (g_pipeSpeed * 0.5); // Move background slower than obstacles
      if(g_gridOffsetX <= -40.0)
        {
         g_gridOffsetX += 40.0; // Infinite loop reset
        }
     }
   
//--- Draw the dynamic background grid
   for(int x = (int)g_gridOffsetX; x < g_chartWidth + 40; x += 40)
     {
      canvas.LineVertical(x, 0, g_chartHeight, CLR_GRID);
     }
   for(int y = 0; y < g_chartHeight; y += 40)
     {
      canvas.LineHorizontal(0, g_chartWidth, y, CLR_GRID);
     }

//--- STEP 2: CALCULATE AND DRAW BASED ON CURRENT STATE
   if(g_gameState == START_SCREEN)
     {
      DrawStartScreen();
     }
   else if(g_gameState == PLAYING)
     {
      UpdatePhysics();
      DrawPipes();
      DrawRocket();
      DrawScore();
     }
   else if(g_gameState == GAME_OVER)
     {
      DrawPipes();
      DrawRocket();
      DrawGameOver();
     }

//--- STEP 3: UPDATE DISPLAY (Send the rendered graphics to the monitor)
   canvas.Update();
  }


Pintando con Matemáticas: Geometría Vectorial

Uno de los mayores obstáculos para los principiantes es pensar que para dibujar un cohete necesitan saber diseño gráfico, crear un archivo .png en Photoshop y cargarlo en MetaTrader. Aunque CCanvas permite cargar imágenes, nosotros vamos a usar Geometría Vectorial.

Esto significa que vamos a construir nuestros personajes y obstáculos uniendo figuras geométricas básicas mediante código. Las ventajas son inmensas: el código es más ligero, se escala perfectamente a cualquier tamaño de la pantalla sin pixelarse, y no necesitamos adjuntar archivos extra.

Vamos a desglosar cómo creamos nuestro cohete (la función DrawRocket). Nuestro cohete no es más que la suma de:

  • Un rectángulo central que hace de fuselaje, dibujado con canvas.FillRectangle(). Le pasamos las coordenadas de la esquina superior izquierda y la inferior derecha.
  • Un triángulo para la punta (el morro del cohete), creado con canvas.FillTriangle(). Esta función requiere tres puntos (X e Y para cada una de las 3 esquinas del triángulo).
  • Dos triángulos adicionales más pequeños que actúan como alerones superior e inferior.
  • Un círculo dibujado con canvas.FillCircle() en el centro del rectángulo para simular la ventana o escotilla.

Lo mejor de este enfoque es que todas las coordenadas de estos dibujos se calculan en base a player_x y player_y (la posición central del jugador). Así, si el jugador cae por la gravedad, el cohete entero se dibuja automáticamente más abajo en el siguiente milisegundo.

//+------------------------------------------------------------------+
//| Vector Engine: Draw the Rocket                                   |
//+------------------------------------------------------------------+
void DrawRocket()
  {
   int px = (int)g_playerX;
   int py = (int)g_playerY;
   
//--- 1. Draw dynamic engine fire if the rocket is jumping (negative velocity)
   if(g_velocityY < 0.0)
     {
      int fire_len = (MathRand() % 15) + 15;
      uchar flicker_alpha = (uchar)((MathRand() % 100) + 100);
      uint clrFireOuter = ColorToARGB(C'255,80,30', flicker_alpha);
      
      //--- Transparent outer flame (Aura)
      canvas.FillTriangle(px - 10, py - 8, px - 10, py + 8, px - 10 - fire_len - 5, py, clrFireOuter);
      //--- Solid inner core flame
      canvas.FillTriangle(px - 10, py - 4, px - 10, py + 4, px - 10 - fire_len, py, CLR_FIRECORE);
     }

//--- 2. Draw the main fuselage using basic geometry
   canvas.FillRectangle(px - 10, py - 6, px + 8, py + 6, CLR_ROCKET); 
   canvas.FillTriangle(px + 8, py - 6, px + 8, py + 6, px + 18, py, CLR_ACCENT); 
   
//--- 3. Draw upper and lower aerodynamic fins
   canvas.FillTriangle(px - 10, py - 6, px - 2, py - 6, px - 10, py - 12, CLR_ACCENT); 
   canvas.FillTriangle(px - 10, py + 6, px - 2, py + 6, px - 10, py + 12, CLR_ACCENT); 
   
//--- 4. Draw the cockpit window
   canvas.FillCircle(px, py, 3, CLR_BG);
  }

//+------------------------------------------------------------------+
//| Vector Engine: Draw the Candles (Obstacles)                      |
//+------------------------------------------------------------------+
void DrawPipes()
  {
   for(int i = 0; i < 3; i++)
     {
      int px = (int)g_pipes[i].x;
      int gap_top = (int)(g_pipes[i].gap_y - g_pipeGap / 2.0);
      int gap_bot = (int)(g_pipes[i].gap_y + g_pipeGap / 2.0);

      //--- BEAR CANDLE (Dropping from the ceiling)
      canvas.FillRectangle(px, 0, px + PIPE_WIDTH, gap_top, CLR_BEAR);
      canvas.LineVertical(px + PIPE_WIDTH / 2, gap_top, gap_top + 20, ColorToARGB(C'150,150,150', 255));
      canvas.Rectangle(px, 0, px + PIPE_WIDTH, gap_top, ColorToARGB(clrBlack, 255)); 

      //--- BULL CANDLE (Rising from the floor)
      canvas.FillRectangle(px, gap_bot, px + PIPE_WIDTH, g_chartHeight, CLR_BULL);
      canvas.LineVertical(px + PIPE_WIDTH / 2, gap_bot - 20, gap_bot, ColorToARGB(C'150,150,150', 255));
      canvas.Rectangle(px, gap_bot, px + PIPE_WIDTH, g_chartHeight, ColorToARGB(clrBlack, 255)); 
     }
  }


Las Leyes de la Física: Gravedad en Código

En el mundo real, si usted deja caer una pelota, esta no baja a una velocidad constante. Comienza a caer lentamente y acelera a medida que se acerca al suelo. Para que nuestro juego se sienta natural y fluido, debemos replicar esta aceleración, y hacerlo en código es sorprendentemente sencillo.

Solo necesitamos tres variables:

  • player_y: La posición vertical actual del cohete.
  • velocity_y: La velocidad actual a la que cae (o sube) el cohete.
  • gravity: Una constante (en nuestro caso 0.6) que empuja todo hacia abajo.

En la función UpdatePhysics(), aplicamos la gravedad en dos sencillos pasos que ocurren 83 veces por segundo. Primero, le sumamos la gravedad a la velocidad (velocity_y += gravity). Segundo, le sumamos esta nueva velocidad a la posición del cohete (player_y += velocity_y).

Al hacer esto, la velocidad se hace cada vez más grande con cada fotograma, creando una curva de caída perfecta (una parábola) en lugar de un movimiento robótico en línea recta.


Detección de Colisiones: Las Cajas Invisibles

Si el cohete se mueve libremente, ¿cómo sabe MetaTrader 5 cuándo hemos chocado contra una vela japonesa? Para resolver esto utilizamos una técnica fundamental en el desarrollo de videojuegos llamada AABB (Axis-Aligned Bounding Box), o como se le conoce coloquialmente: cajas de colisión.

Piense en esto como dibujar cajas rectangulares invisibles alrededor de nuestros objetos. Para saber si el cohete se estrelló, el sistema evalúa dos condiciones:

  • ¿Están superpuestos en el eje X (horizontal)? Verificamos si el borde derecho del cohete ha cruzado el borde izquierdo de la vela, y si el borde izquierdo del cohete aún no ha salido del borde derecho de la vela.
  • ¿Están superpuestos en el eje Y (vertical)? Si el cohete está dentro de los límites horizontales de la vela, comprobamos si su altura toca la parte superior de la vela roja (Bear) o la parte inferior de la vela verde (Bull).

Si ambas condiciones son verdaderas al mismo tiempo... ¡Boom! Significa que las cajas invisibles se han tocado. En ese exacto milisegundo llamamos a la función TriggerGameOver(), reproducimos el doloroso sonido de desconexión de MetaTrader 5 con PlaySound("disconnect.wav") y mostramos el letrero de liquidación.

//+------------------------------------------------------------------+
//| Physics and Collision Updates                                    |
//+------------------------------------------------------------------+
void UpdatePhysics()
  {
//--- Apply gravity to the falling speed
   g_velocityY += GRAVITY;
//--- Move the rocket along the Y axis
   g_playerY += g_velocityY;

//--- Detect collision with the ceiling or the floor
   if(g_playerY < 0.0 || g_playerY > g_chartHeight)
     {
      TriggerGameOver();
      return;
     }

//--- Update obstacles and check for collisions
   for(int i = 0; i < 3; i++)
     {
      g_pipes[i].x -= g_pipeSpeed;

      //--- If the obstacle leaves the screen, recycle it to the right
      if(g_pipes[i].x < -PIPE_WIDTH)
        {
         g_pipes[i].x += g_pipeSpacing * 3.0;
         g_pipes[i].gap_y = (double)(MathRand() % (g_chartHeight - 300) + 150); // New random gap height
         g_pipes[i].passed = false;
        }

      //--- AABB Collision Detection (Hitbox evaluation)
      // Horizontal check
      if(g_playerX + 15.0 > g_pipes[i].x && g_playerX - 15.0 < g_pipes[i].x + PIPE_WIDTH)
        {
         //--- Vertical check (Did it hit the upper bear candle or lower bull candle?)
         if(g_playerY - 12.0 < g_pipes[i].gap_y - g_pipeGap / 2.0 || g_playerY + 12.0 > g_pipes[i].gap_y + g_pipeGap / 2.0)
           {
            TriggerGameOver();
            return;
           }
        }

      //--- Score counter (Executed only once when passing an obstacle)
      if(!g_pipes[i].passed && g_playerX > g_pipes[i].x + PIPE_WIDTH)
        {
         g_score++;
         g_pipes[i].passed = true;
         PlaySound("ok.wav");

         //--- DYNAMIC DIFFICULTY RAMP-UP (Triggered every 10 points)
         if(g_score % 10 == 0)
           {
            if(g_pipeSpeed < 12.0)
              {
               g_pipeSpeed += 1.5;      // Increase market volatility (speed)
              }

            if(g_pipeGap > 110)
              {
               g_pipeGap -= 20;         // Decrease safety gap
              }

            if(g_pipeSpacing > 250)
              {
               g_pipeSpacing -= 30;     // Decrease distance between candles
              }
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Trigger Game Over State                                          |
//+------------------------------------------------------------------+
void TriggerGameOver()
  {
   g_gameState = GAME_OVER;

//--- Save the All-Time High score in memory
   if(g_score > g_bestScore)
     {
      g_bestScore = g_score;
     }

//--- Play the classic MT5 disconnection sound as a Margin Call alert
   PlaySound("disconnect.wav");
  }


Dificultad Dinámica: El Mercado se vuelve Volátil

Un buen minijuego, al igual que un buen mercado financiero, debe cambiar su volatilidad para no aburrir al usuario. No podemos dejar que la velocidad de las velas sea siempre la misma.

Para aumentar la dificultad progresivamente, utilizamos una de las herramientas más útiles de la programación: el Operador Módulo (%). El módulo nos devuelve el resto de una división.

Por ejemplo, usamos la condición if(score % 10 == 0). Esto significa que cada vez que nuestro Profit llegue a un múltiplo de 10 (10, 20, 30...), el juego ejecutará una rutina que hace tres cosas para complicarnos la vida:

  • Aumenta la velocidad a la que se mueven las velas hacia la izquierda.
  • Reduce el hueco vertical entre la vela roja y la verde.
  • Reduce la distancia horizontal entre un obstáculo y el siguiente.


Tomando el Control: Captura de Teclado (Eventos)

Un juego no es nada sin interactividad. MetaTrader 5 proporciona una función nativa llamada OnChartEvent() que escucha todo lo que el usuario hace (clics del ratón, movimientos y teclas presionadas).

En nuestro caso, queremos escuchar el evento CHARTEVENT_KEYDOWN (Tecla presionada). Cada tecla en su teclado tiene un código numérico asociado (lparam). El número 32 corresponde a la barra espaciadora y el 38 a la flecha hacia arriba.

Cuando el sistema detecta una de estas teclas y el juego está en estado JUGANDO, impulsamos el cohete. ¿Cómo? Simplemente inyectando un valor negativo gigante a nuestra variable velocity_y (ej. -9.5).

Como recordará del primer apartado, en nuestra pantalla la coordenada 0 está arriba. Al aplicar una velocidad negativa, el cohete salta hacia arriba, superando momentáneamente la fuerza de la gravedad.

//+------------------------------------------------------------------+
//| Chart Event Handler (Keyboard Inputs)                            |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
//--- Check if the event is a keyboard key press
   if(id == CHARTEVENT_KEYDOWN)
     {
      //--- Check if the pressed key is SPACE (32) or UP ARROW (38)
      if(lparam == 32 || lparam == 38)
        {
         if(g_gameState == START_SCREEN)
           {
            ResetGame();
            g_gameState = PLAYING;
           }
         else
           {
            if(g_gameState == PLAYING)
              {
               //--- Apply upward thrust fighting gravity
               g_velocityY = JUMP_STRENGTH;
              }
            else
              {
               if(g_gameState == GAME_OVER)
                 {
                  g_gameState = START_SCREEN;
                 }
              }
           }
        }
     }
  }


Conclusión: El Primer Paso de un Gran Proyecto

Hemos recorrido los fundamentos necesarios para pasar de la confusión inicial a un prototipo funcional sobre MetaTrader 5. Concretamente, implementamos y entendimos: el sistema de coordenadas de CCanvas, la creación de un lienzo transparente con ARGB, un game loop independiente de ticks mediante EventSetMillisecondTimer, una máquina de estados (start → playing → game over), la renderización vectorial de objetos (rectángulos, triángulos, círculos), física simple (gravedad y salto), detección de colisiones AABB y captura de teclado con OnChartEvent.

Resultado práctico: dispone de un prototipo compilable —Crazy Scalper— que

  • dibuja y actualiza la escena sin depender del mercado,
  • soporta transparencias y efecto de paralaje,
  • entrega un esqueleto listo para integrar UI interactivas en sus EAs (canvas + ciclo + eventos + estado + colisiones).

Esto es solo la base. En próximos artículos abordaremos persistencia (.bin) para guardar récords, refactorización orientada a objetos para ampliar avatares y enemigos, y optimización/UX para reducir consumo y mejorar ergonomía visual. ¿Cuál de estas mejoras le gustaría que tratemos primero?

Puede descargar el código fuente completo de esta primera versión en el archivo adjunto. ¡Compile, juegue, rompa el código para ver qué sucede y experimente creando sus propias reglas! 

Archivos adjuntos |
Crazy_Scalper.mq5 (17.68 KB)
Redes neuronales en el trading: Segmentación periódica adaptativa (Final) Redes neuronales en el trading: Segmentación periódica adaptativa (Final)
Le propongo sumergirse en el apasionante mundo de LightGTS, un framework de predicción de series temporales ligero pero potente que combina la convolución adaptativa y la codificación RoPE con métodos de atención innovadores. En el artículo de hoy, encontrará una descripción detallada de todos los componentes, desde la creación de parches hasta una compleja combinación de asesores expertos en un decodificador, listo para su integración en proyectos MQL5. ¡Descubra cómo LightGTS lleva el trading automatizado al siguiente nivel!
Creación de un Panel de administración de operaciones en MQL5 (Parte XII): Integración de una calculadora de valores Forex Creación de un Panel de administración de operaciones en MQL5 (Parte XII): Integración de una calculadora de valores Forex
El cálculo preciso de los valores clave de las operaciones es una parte indispensable del flujo de trabajo de cualquier operador. En este artículo, analizaremos la integración de una potente herramienta —la calculadora de Forex— en el Panel de gestión de operaciones, lo que amplía aún más la funcionalidad de nuestro sistema «Trading Administrator» de múltiples paneles. A la hora de realizar operaciones, es fundamental determinar de forma eficaz el riesgo, el tamaño de la posición y el beneficio potencial, y esta nueva función está diseñada para que ese proceso sea más rápido e intuitivo dentro del panel. Veamos cómo se aplica MQL5 en la creación de paneles de trading avanzados.
De novato a experto: Noticias animadas utilizando MQL5 (IV) Análisis de mercado sobre modelos de IA alojados localmente De novato a experto: Noticias animadas utilizando MQL5 (IV) Análisis de mercado sobre modelos de IA alojados localmente
En esta discusión, analizaremos cómo autoalojar modelos de IA de código abierto y utilizarlos para obtener información sobre el mercado. Esto forma parte de nuestro esfuerzo continuo por ampliar el News Headline EA, con la introducción de una franja «AI Insights» que lo convierte en una herramienta de asistencia con múltiples integraciones. La versión mejorada del Asesor Experto (EA) tiene como objetivo mantener informados a los operadores a través de eventos del calendario, noticias financieras de última hora, indicadores técnicos y, ahora, perspectivas de mercado generadas por IA, ofreciendo así un apoyo oportuno, variado e inteligente para la toma de decisiones de trading. Únete a la conversación mientras exploramos estrategias prácticas de integración y cómo MQL5 puede colaborar con recursos externos para crear un terminal de trabajo para trading potente e inteligente.
Análisis de las brechas temporales de precios en MQL5 (Parte II): Creamos un mapa de calor de la distribución de liquidez a lo largo del tiempo Análisis de las brechas temporales de precios en MQL5 (Parte II): Creamos un mapa de calor de la distribución de liquidez a lo largo del tiempo
Hoy veremos una guía detallada sobre cómo crear un indicador de mapa de calor para MetaTrader 5 que visualice la distribución de precios a lo largo del tiempo como un mapa de calor. El artículo revela la base matemática del análisis de densidad temporal, donde cada nivel de precio está coloreado desde el rojo (tiempo mínimo de estancia) hasta el azul (tiempo máximo de estancia).